From 55377c5b6c32f5b2d789bfd08f69b9b874f2f18d Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Fri, 9 Oct 2026 02:11:07 -0700 Subject: [PATCH] Give each template a function scope ESLint's no-use-before-define reports a component that renders itself: const Foo = ; class Bar extends Component { } It does so even with `{ functions: false, classes: false, variables: false }` (eslint-config-standard), because it only exempts references from a separate execution context, and a template's block scope shares its upper scope's. A template compiles to a closure (`scope: () => ({ Foo })`), so give it a function scope instead. On the TypeScript path, FunctionScope declares an implicit `arguments` and scans the body for a "use strict" directive, which throws on a template's body when the upper scope isn't strict (a script). A TemplateScope subclass removes `arguments` and passes `isMethodDefinition` to skip the scan; the eslint-scope path uses the base Scope, which declares nothing. --- src/parser/transforms.js | 33 ++++++- test-projects/gts/.eslintrc.cjs | 5 +- test-projects/gts/src/use-before-define.gts | 15 ++++ tests/parser.test.js | 95 +++++++++++++++++++++ 4 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/parser/transforms.js b/src/parser/transforms.js index 3f9ad82..e6815f2 100644 --- a/src/parser/transforms.js +++ b/src/parser/transforms.js @@ -15,6 +15,7 @@ const mathMLTagsSet = new Set(mathmlTagNames); const require = createRequire(import.meta.url); let TypescriptScope = null; +let TypescriptTemplateScope = null; try { const parserPath = require.resolve('@typescript-eslint/parser'); // eslint-disable-next-line n/no-unpublished-require @@ -22,6 +23,26 @@ try { paths: [parserPath], }); TypescriptScope = require(scopeManagerPath); + TypescriptTemplateScope = class TemplateScope extends TypescriptScope.FunctionScope { + constructor(scopeManager, upperScope, block) { + // `isMethodDefinition` makes it strict without scanning the body for a + // "use strict" directive, which throws on a template's body. + super(scopeManager, upperScope, block, true); + // A template has no `arguments`, unlike the function FunctionScope models. + const args = this.set.get('arguments'); + if (args) { + this.set.delete('arguments'); + this.variables.splice(this.variables.indexOf(args), 1); + } + } + + // Never called for a template scope: only closing a scope during analysis + // calls it, and templates get their scopes after that. The base would + // throw on a template's `body`, so guard it anyway. + isValidResolution() { + return true; + } + }; } catch { // not available } @@ -92,11 +113,15 @@ function createBlockScope(node, path, scopeManager, isTypescript) { : new Scope(scopeManager, 'block', upperScope, node); } -// Give each template a scope of its own. Otherwise typescript-eslint's -// no-use-before-define treats a reference in `const Foo =