99 type AnalyzeOptions ,
1010 type ScopeManager as TSESLintScopeManager ,
1111} from '@typescript-eslint/scope-manager' ;
12- import { SOURCE_CODE } from './source_code.js' ;
12+ import { ast , initAst } from './source_code.js' ;
13+ import { assertIs } from './utils.js' ;
1314
1415type Identifier =
1516 | ESTree . IdentifierName
@@ -19,52 +20,88 @@ type Identifier =
1920 | ESTree . TSThisParameter
2021 | ESTree . TSIndexSignatureName ;
2122
23+ // TS-ESLint `ScopeManager` for current file.
24+ // Created lazily only when needed.
25+ let tsScopeManager : TSESLintScopeManager | null = null ;
26+
27+ // Options for TS-ESLint's `analyze` method.
28+ // `sourceType` property is set before calling `analyze`.
29+ interface AnalyzeOptionsWithNullableSourceType extends Omit < AnalyzeOptions , 'sourceType' > {
30+ sourceType : AnalyzeOptions [ 'sourceType' ] | number ;
31+ }
32+
33+ const analyzeOptions : AnalyzeOptionsWithNullableSourceType = {
34+ globalReturn : false ,
35+ jsxFragmentName : null ,
36+ jsxPragma : 'React' ,
37+ lib : [ 'esnext' ] ,
38+ sourceType : null ,
39+ } ;
40+
41+ /**
42+ * Initialize TS-ESLint `ScopeManager` for current file.
43+ */
44+ function initTsScopeManager ( ) {
45+ if ( ast === null ) initAst ( ) ;
46+
47+ analyzeOptions . sourceType = ast . sourceType ;
48+ assertIs < AnalyzeOptions > ( analyzeOptions ) ;
49+ // The effectiveness of this assertion depends on our alignment with ESTree.
50+ // It could eventually be removed as we align the remaining corner cases and the typegen.
51+ // @ts -expect-error // TODO: Our types don't quite align yet
52+ tsScopeManager = analyze ( ast , analyzeOptions ) ;
53+ }
54+
55+ /**
56+ * Discard TS-ESLint `ScopeManager`, to free memory.
57+ */
58+ export function resetScopeManager ( ) {
59+ tsScopeManager = null ;
60+ }
61+
2262/**
2363 * @see https://eslint.org/docs/latest/developer-guide/scope-manager-interface#scopemanager-interface
2464 */
25- // This is a wrapper class around the @typescript -eslint/scope-manager package.
65+ // This is a wrapper around ` @typescript-eslint/scope-manager` package's `ScopeManager` class .
2666// We want to control what APIs are exposed to the user to limit breaking changes when we switch our implementation.
27- export class ScopeManager {
28- #scopeManager: TSESLintScopeManager ;
29-
30- constructor ( ast : ESTree . Program ) {
31- const defaultOptions : AnalyzeOptions = {
32- globalReturn : false ,
33- jsxFragmentName : null ,
34- jsxPragma : 'React' ,
35- lib : [ 'esnext' ] ,
36- sourceType : ast . sourceType ,
37- } ;
38- // The effectiveness of this assertion depends on our alignment with ESTree.
39- // It could eventually be removed as we align the remaining corner cases and the typegen.
40- // @ts -expect-error // TODO: our types don't quite align yet
41- this . #scopeManager = analyze ( ast , defaultOptions ) ;
42- }
43-
67+ //
68+ // Only one file is linted at a time, so we can reuse a single object for all files.
69+ //
70+ // This has advantages:
71+ // 1. Reduce object creation.
72+ // 2. Property accesses don't need to go up prototype chain, as they would for instances of a class.
73+ // 3. No need for private properties, which are somewhat expensive to access - use top-level variables instead.
74+ //
75+ // Freeze the object to prevent user mutating it.
76+ export const SCOPE_MANAGER = Object . freeze ( {
4477 /**
4578 * All scopes
4679 */
4780 get scopes ( ) : Scope [ ] {
48- // @ts -expect-error // TODO: our types don't quite align yet
49- return this . #scopeManager. scopes ;
50- }
81+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
82+ // @ts -expect-error // TODO: Our types don't quite align yet
83+ return tsScopeManager . scopes ;
84+ } ,
5185
5286 /**
5387 * The root scope
5488 */
5589 get globalScope ( ) : Scope | null {
56- return this . #scopeManager. globalScope as any ;
57- }
90+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
91+ // @ts -expect-error // TODO: Our types don't quite align yet
92+ return tsScopeManager . globalScope ;
93+ } ,
5894
5995 /**
6096 * Get the variables that a given AST node defines. The gotten variables' `def[].node`/`def[].parent` property is the node.
6197 * If the node does not define any variable, this returns an empty array.
6298 * @param node An AST node to get their variables.
6399 */
64100 getDeclaredVariables ( node : ESTree . Node ) : Variable [ ] {
65- // @ts -expect-error // TODO: our types don't quite align yet
66- return this . #scopeManager. getDeclaredVariables ( node ) ;
67- }
101+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
102+ // @ts -expect-error // TODO: Our types don't quite align yet
103+ return tsScopeManager . getDeclaredVariables ( node ) ;
104+ } ,
68105
69106 /**
70107 * Get the scope of a given AST node. The gotten scope's `block` property is the node.
@@ -75,10 +112,13 @@ export class ScopeManager {
75112 * If `inner` is `true` then this returns the innermost scope.
76113 */
77114 acquire ( node : ESTree . Node , inner ?: boolean ) : Scope | null {
78- // @ts -expect-error // TODO: our types don't quite align yet
79- return this . #scopeManager. acquire ( node , inner ) ;
80- }
81- }
115+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
116+ // @ts -expect-error // TODO: Our types don't quite align yet
117+ return tsScopeManager . acquire ( node , inner ) ;
118+ } ,
119+ } ) ;
120+
121+ export type ScopeManager = typeof SCOPE_MANAGER ;
82122
83123export interface Scope {
84124 type : ScopeType ;
@@ -169,7 +209,8 @@ export function isGlobalReference(node: ESTree.Node): boolean {
169209 return false ;
170210 }
171211
172- const globalScope = SOURCE_CODE . scopeManager . scopes [ 0 ] ;
212+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
213+ const globalScope = tsScopeManager . scopes [ 0 ] ;
173214 if ( ! globalScope ) return false ;
174215
175216 // If the identifier is a reference to a global variable, the global scope should have a variable with the name.
@@ -201,7 +242,9 @@ export function isGlobalReference(node: ESTree.Node): boolean {
201242 */
202243export function getDeclaredVariables ( node : ESTree . Node ) : Variable [ ] {
203244 // ref: https://github.com/eslint/eslint/blob/e7cda3bdf1bdd664e6033503a3315ad81736b200/lib/languages/js/source-code/source-code.js#L904
204- return SOURCE_CODE . scopeManager . getDeclaredVariables ( node ) ;
245+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
246+ // @ts -expect-error // TODO: Our types don't quite align yet
247+ return tsScopeManager . getDeclaredVariables ( node ) ;
205248}
206249
207250/**
@@ -215,21 +258,25 @@ export function getScope(node: ESTree.Node): Scope {
215258 throw new TypeError ( 'Missing required argument: node.' ) ;
216259 }
217260
218- const { scopeManager } = SOURCE_CODE ;
261+ if ( tsScopeManager === null ) initTsScopeManager ( ) ;
262+
219263 const inner = node . type !== 'Program' ;
220264
221265 // Traverse up the AST to find a `Node` whose scope can be acquired.
222266 for ( let current : any = node ; current ; current = current . parent ) {
223- const scope = scopeManager . acquire ( current , inner ) ;
267+ const scope = tsScopeManager . acquire ( current , inner ) ;
224268
225269 if ( scope ) {
226270 if ( scope . type === 'function-expression-name' ) {
271+ // @ts -expect-error // TODO: Our types don't quite align yet
227272 return scope . childScopes [ 0 ] ;
228273 }
229274
275+ // @ts -expect-error // TODO: Our types don't quite align yet
230276 return scope ;
231277 }
232278 }
233279
234- return scopeManager . scopes [ 0 ] ;
280+ // @ts -expect-error // TODO: Our types don't quite align yet
281+ return tsScopeManager . scopes [ 0 ] ;
235282}
0 commit comments