Skip to content

Latte 3.1 support - #1

Merged
WerySkok merged 27 commits into
mainfrom
latte-3.1-support
Oct 28, 2026
Merged

WerySkok merged 27 commits into
mainfrom
latte-3.1-support

Conversation

@WerySkok

@WerySkok WerySkok commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Fork of the extension with Latte 3.1 support while keeping Latte 2.11 / 3.0 support.

Changes

  • compiler: Latte 3.1 TemplateGenerator::buildClass()/generateCode() split, pos -> line mapping, class comment injection (also for Latte 3.0.27)
  • filters/functions: Latte 3.1 first-class callables are routed through array callables; non-public ones stay closures
  • escaping: restored type checks for Latte 3.1 HtmlHelpers / Helpers / XmlHelpers; more precise slice() and escapeJs() typing
  • layouts: quoted, bare and absolute paths, {layout none}, missing-layout diagnostics
  • cache: compiled templates are required only once; latte config is part of the collector cache key
  • new options: latte.templateTypes, latte.maxTemplateVariants, latte.breakIncludeCycles, latte.resolveTypes
  • tests: Latte 2.11 / 3.0 / 3.1 and PHP 7.4 - 8.5 matrix

CI

All 49 checks pass: codesniffer, PHPStan level max (normal + lowest dependencies), PHPUnit (normal + lowest dependencies), syntax checker and extensions finder.

Fork notes

  • scheduled workflow runs are disabled in this fork
  • composer.json pins version: 0.20.1 for path repositories

@WerySkok WerySkok closed this Oct 8, 2026
@WerySkok WerySkok reopened this Oct 8, 2026
@WerySkok
WerySkok marked this pull request as ready for review October 8, 2026 22:34
@WerySkok
WerySkok marked this pull request as draft October 8, 2026 22:36
@WerySkok WerySkok closed this Oct 8, 2026
@WerySkok WerySkok reopened this Oct 8, 2026
@WerySkok WerySkok changed the title WIP: Latte 3.1 support Latte 3.1 support Oct 28, 2026
…preg_match

- add LatteVersion::isLatte31() and use it instead of method_exists() which
  PHPStan constant-folds to true under Latte 3.1
- ignore the legacy TemplateGenerator::generate()/buildClass()/generateCode()
  calls for the Latte version that does not define them, following the
  existing cross-version ignoreErrors in phpstan.neon
- narrow preg_match result in LayoutPathResolver with isset() so PHPStan
  stops reporting offsets on the inferred array shape
…n PHPDoc

Latte 3.1 registers built-in filters and functions as first-class callables,
so their closure types (resolved by ClosureTypeFactory) now embed bare
array/iterable/Traversable/Generator types. Those were emitted verbatim into
the @var doc of the generated $__filters__/$__functions__ variables, making
every compiled template report missingType errors at level 6+.

Add TypeHelper::toPhpDocNode() which expands such bare iterables to explicit
mixed type arguments before printing. Fixes 28 failing tests.
Latte 3.1 compiles escaping to Latte\Runtime\HtmlHelpers::escapeText() whose
$s parameter is mixed, so the FilterString checks from the old
Latte\Runtime\Filters::escapeHtmlText() stub no longer fired. Stub
HtmlHelpers::escapeText with the same FilterString type and teach
EscapeErrorTransformer to translate its errors to the
"Cannot convert X to HTML string." message.
… through array callables

Latte 3.1 registers built-in filters and functions as first-class callables
(Closure objects) instead of [class, method] arrays. The visitors treated
them as plain closures, so calls were generated on closure variables and the
Latte stubs (e.g. stubs/Latte/Essential/Filters.stub) no longer applied.

Detect closures that wrap a method via ReflectionFunction scope class and
convert them back to [class, method], so static methods are called directly
and stubs apply again.

Latte 3.1 also adds native types to the built-in filters; PHPStan stubs can
only narrow native types, so the FilterString stub union no longer applies
to |lower/|upper/|capitalize and the native |slice return type (Generator)
leaks into escape checks. Expectations for Latte 3.1 are adjusted accordingly,
Latte 2 and 3.0 expectations are unchanged.
- use the correct Nette\Utils\FileSystem class name (Filesystem references)
- cover list<Latte\Extension> in the getExtensions() is_array ignore
- drop now-redundant is_string()/is_array()/count() checks
- align FakePresenterFactory::setFakeMapping() mapping shape with
  PresenterFactory::setMapping(); parent already validates the mapping, so
  the duplicated 2-element array branch was unreachable
- drop ignores that no longer match and mark the get_class one reportUnmatched
PHPStan 2.3 reports new dead-code, repeated-assignment, implicit-array and
coalesce errors on generated template code, and nette/forms 3.3 changed
FormsLatte\Runtime::get() to return Control|Container, which made generated
control lookups lose their BaseControl type.

- add a Runtime::get() stub with a conditional return type so control lookups
  keep resolving to BaseControl (Container lookups are unaffected)
- update Variables/Forms/Links/NoMapping fixture expectations for the new
  PHPStan 2.3 errors; the separated-phpstan-command variant does not report
  them, so they are gated with usesSeparatedPhpStanCommand()
- gate the Latte 3.1-only slice() Generator expectations with isLatte31()

Latte 2 and 3.0 expectations are preserved.
PHP 7.4 CI installs Latte 2.11.x and analyses with phpVersion 70400,
where the PHP 8 string helpers used across the collectors and node
visitors are unknown symbols. Require symfony/polyfill-php80 so the
functions are discovered on PHP 7.4 too.

Fixes 26 'Function str_contains/str_starts_with not found.' errors in
the PHPStan (normal) and PHPStan (lowest) matrix jobs.
On Latte 3.0/2.11 TemplateGenerator::generateCode() is unknown, so its
return value is mixed; assert string before str_replace(). On PHPStan
2.1 ClassReflection::getMethod() received mixed from filter/function
array callables; add an is_string() guard (ignored as always-true on
PHPStan 2.3 where the phpdoc already narrows).

Fixes Latte3Compiler.php:100 and ChangeFilters/ChangeFunctionsNodeVisitor
getMethod() errors in the PHPStan (normal) PHP 8.0/8.1 and PHPStan
(lowest) jobs.
The PHPunit matrix resolves different dependencies per PHP version:
PHPStan 2.1.33 (lowest), Latte 2.11.x (PHP 7.4, lowest <= 8.3), Latte
3.0.x (PHP 8.0/8.1, lowest 8.4/8.5), nette/forms 3.1.x (PHP <= 8.1),
3.2.x (PHP 8.2) and 3.3.x (PHP 8.3+).

- DependencyVersion detects PHPStan >= 2.3 via UnusedVariableRule (inside
  RuleTestCase the PHPStan phar ships its own Composer\InstalledVersions
  without package versions) and nette/forms >= 3.3 via the removal of
  FormsLatte\Runtime::item()
- testVariables/testComponents/testSnippets/testLinks: gate PHPStan
  2.3-only dead-code errors and add the Latte 2-only $iterations errors
- testForms: add a Latte 3.1 + nette/forms 3.2 branch (item() still used
  there) and the nette/forms 3.1 extras for Latte 2/3.0

Verified locally on all four dependency shapes (Latte 2.11.7/nf 3.1.15/
PHPStan 2.3.1, Latte 3.0.26/nf 3.1.15/PHPStan 2.3.1, Latte 3.1.6/nf 3.2.9/
PHPStan 2.3.1, Latte 3.1.6/nf 3.3.0/PHPStan 2.3.1) plus Latte 2.11.7/
PHPStan 2.1.33: 82/82 tests pass.
TypeHelper::toPhpDocNode() replaced the base identifier of already generic
types (e.g. "array" in "array<string>"), which threw a TypeError inside
NodeTraverser and aborted analysis of every compiled template. Expand bare
iterables only and leave typed generics untouched.

Resolve first-class callables of instance methods via the bound object class
so the most derived implementation is analysed, as with array callables.

Add TypeHelper unit tests and an inherited first-class callable filter
fixture that fails without the fix.
Latte 3.0.27 normalizes multi-line template names to null, so passing the
generated class comment as TemplateGenerator::generate()'s template name
dropped the class docblock and left $this->global typed as mixed in every
compiled template.

Pass null and inject the comment before the class declaration for both the
3.0 and 3.1 generator APIs, matching the previous 3.0 output.

Also keep the first-class callable fixture parseable by PHP 7.4 (PHPDoc
@return static instead of the native return type).
- stub Latte 3.1 runtime escaping helpers (Helpers, XmlHelpers, HtmlHelpers)
  so escape type errors are reported again, and teach the error transformer
  about the new helper classes
- respect {layout none} and report missing explicit layouts instead of
  silently replacing them with @layout.latte
- include the latte config in the collector cache key so templateTypes and
  other options invalidate cached collector results
- escape and verify the injected generated class comment
- keep non-public first-class callables as closures, as generated template
  classes cannot call them
- add regression tests for escapes, layout resolution and private filters
- document templateTypes, maxTemplateVariants and breakIncludeCycles
- add a dynamic static return type extension for slice() so strings stay
  strings and arrays keep their value type instead of leaking Generator
  into escape checks on Latte 3.1
- stop narrowing escapeJs to FilterString: Latte JSON-encodes arrays and
  objects, so those are valid arguments
- pass the engine feature map to TemplateGenerator::buildClass() so
  ScopedLoopVariables and other 3.1 features affect generated code; ignore
  the unset() false positive produced by Latte's scoped-loop code
- add a ScopedLoopVariables engine-bootstrap regression test and update
  slice/escape expectations
phpstan.neon now skips Feature::cases() and Engine::hasFeature() when
self-analysis runs against Latte 2.11/3.0, as it already does for the
version-specific TemplateGenerator methods.
- report a missing layout as a regular rule error instead of crashing on a
  non-existent template path; guard all layout template additions with
  is_file() and make ErrorBuilder fall back to an existing file
- parse layout tags with quote awareness: only single non-interpolated
  string literals (or bare file names) are paths, expressions fall back to
  @layout.latte; support absolute paths and treat quoted none/auto as files
- include engine feature state in the Latte3Compiler cache key
- keep running semantic scope visitors when resolveTypes=false, with a
  fallback for filter/function class detection and snippet driver rewrite
- normalize '/* pos */' comments via token_get_all so user string literals
  are not rewritten
- give the Latte 3.1 phantom Filters::escapeJs stub an explicit mixed type
- add tests for missing layouts, layout expressions, maxTemplateVariants,
  breakIncludeCycles, resolveTypes=false, templateTypes and compiler cache
  key; fix the first-class callable @requires annotation
- ignore the mixed feature-name concatenation when self-analysing against
  Latte 2.11/3.0
- import PREG_OFFSET_CAPTURE for the coding standard
- require ext-tokenizer now that the compiler uses token_get_all()
- reject non-file setFile() template paths with latte.notFound instead of
  letting them crash compilation, and guard LayoutPathResolver::resolve()
  against non-file inputs
- treat extensionless unquoted layout names ({layout main}) as literal paths
  again, while keeping interpolated/quoted expressions dynamic
- emit warnings when maxTemplateVariants or breakIncludeCycles skip a
  variant/include so analysis gaps are visible, preserving explicit
  is_warning metadata in ErrorBuilder
- document the resolveTypes tradeoff for forms and the new skipped-variant
  warnings
- add missing setFile and extensionless layout regression tests
@WerySkok
WerySkok marked this pull request as ready for review October 28, 2026 09:07
@WerySkok
WerySkok merged commit 4b436b8 into main Oct 28, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant