@@ -107,18 +107,61 @@ function isDateExpression(node: unknown, isDateName: (name: string) => boolean):
107107 return false
108108}
109109
110- /** Lexical scope for Date-typed bindings; lookups walk the parent chain. */
110+ /**
111+ * Lexical scope for Date-typed bindings; lookups walk the parent chain.
112+ *
113+ * `localNames` holds every name the scope binds, Date-typed or not, so a lookup
114+ * stops at the nearest declaring scope instead of reaching past a shadow.
115+ */
111116interface Scope {
112117 parent : Scope | null
113118 dateNames : Set < string >
119+ localNames : Set < string >
114120}
115121
122+ const createScope = ( parent : Scope | null ) : Scope => ( {
123+ parent,
124+ dateNames : new Set ( ) ,
125+ localNames : new Set ( ) ,
126+ } )
127+
128+ /**
129+ * Resolves `name` to the nearest scope that binds it.
130+ *
131+ * `dateNames` is consulted before `localNames` at every level so the resolution
132+ * fixpoint still converges: a local binding not *yet* proven to be a Date blocks
133+ * the walk on this pass, and a later pass finds it once proven.
134+ */
116135function hasDateName ( scope : Scope , name : string ) : boolean {
117- for ( let current : Scope | null = scope ; current ; current = current . parent )
136+ for ( let current : Scope | null = scope ; current ; current = current . parent ) {
118137 if ( current . dateNames . has ( name ) ) return true
138+ if ( current . localNames . has ( name ) ) return false
139+ }
119140 return false
120141}
121142
143+ /** Records every identifier a binding pattern introduces, however nested. */
144+ function collectBoundNames ( node : unknown , into : Set < string > ) : void {
145+ if ( ! isSyntaxNode ( node ) ) return
146+ if ( node . type === 'Identifier' ) {
147+ if ( typeof node . name === 'string' ) into . add ( node . name )
148+ return
149+ }
150+ if ( node . type === 'ObjectPattern' && Array . isArray ( node . properties ) ) {
151+ for ( const property of node . properties ) {
152+ if ( ! isSyntaxNode ( property ) ) continue
153+ collectBoundNames ( property . type === 'RestElement' ? property . argument : property . value , into )
154+ }
155+ return
156+ }
157+ if ( node . type === 'ArrayPattern' && Array . isArray ( node . elements ) ) {
158+ for ( const element of node . elements ) collectBoundNames ( element , into )
159+ return
160+ }
161+ if ( node . type === 'AssignmentPattern' ) collectBoundNames ( node . left , into )
162+ if ( node . type === 'RestElement' ) collectBoundNames ( node . argument , into )
163+ }
164+
122165const FUNCTION_TYPES = new Set ( [
123166 'FunctionDeclaration' ,
124167 'FunctionExpression' ,
@@ -302,7 +345,7 @@ export function analyzeSource(source: string, file = 'source.ts'): FileAnalysis
302345 if ( bindings . tags . size === 0 && bindings . namespaces . size === 0 ) return { violations : [ ] }
303346
304347 const dateTypeFields = collectDateTypeFields ( program )
305- const rootScope : Scope = { parent : null , dateNames : new Set ( ) }
348+ const rootScope : Scope = createScope ( null )
306349 const candidates : BindingCandidate [ ] = [ ]
307350 const checks : CheckSite [ ] = [ ]
308351
@@ -336,7 +379,8 @@ export function analyzeSource(source: string, file = 'source.ts'): FileAnalysis
336379 for ( const property of pattern . properties ) {
337380 if ( ! isSyntaxNode ( property ) || property . type !== 'ObjectProperty' ) continue
338381 const key = isSyntaxNode ( property . key ) ? property . key . name : undefined
339- const value = isSyntaxNode ( property . value ) ? property . value : undefined
382+ const raw = isSyntaxNode ( property . value ) ? property . value : undefined
383+ const value = raw ?. type === 'AssignmentPattern' && isSyntaxNode ( raw . left ) ? raw . left : raw
340384 if ( typeof key !== 'string' || ! fields . has ( key ) ) continue
341385 if ( value ?. type === 'Identifier' && typeof value . name === 'string' )
342386 scope . dateNames . add ( value . name )
@@ -348,6 +392,7 @@ export function analyzeSource(source: string, file = 'source.ts'): FileAnalysis
348392 for ( const raw of fn . params ) {
349393 if ( ! isSyntaxNode ( raw ) ) continue
350394 const param = raw . type === 'AssignmentPattern' && isSyntaxNode ( raw . left ) ? raw . left : raw
395+ collectBoundNames ( param , scope . localNames )
351396 if ( param . type === 'Identifier' && typeof param . name === 'string' ) {
352397 if ( isDateAnnotation ( param . typeAnnotation ) ) scope . dateNames . add ( param . name )
353398 } else if ( param . type === 'ObjectPattern' ) {
@@ -361,14 +406,15 @@ export function analyzeSource(source: string, file = 'source.ts'): FileAnalysis
361406 const visit = ( node : SyntaxNode , parentScope : Scope , parentStatementLine : number ) => {
362407 let scope = parentScope
363408 if ( FUNCTION_TYPES . has ( node . type ) ) {
364- scope = { parent : parentScope , dateNames : new Set ( ) }
409+ scope = createScope ( parentScope )
365410 bindParameters ( node , scope )
366411 }
367412 const statementLine =
368413 STATEMENT_TYPE . test ( node . type ) && node . loc ? node . loc . start . line : parentStatementLine
369414
370415 if ( node . type === 'VariableDeclarator' && isSyntaxNode ( node . id ) ) {
371416 const target = node . id
417+ collectBoundNames ( target , scope . localNames )
372418 if ( target . type === 'Identifier' && typeof target . name === 'string' ) {
373419 candidates . push ( {
374420 scope,
0 commit comments