@@ -94,6 +94,11 @@ interface ShellCommandScan {
9494 nameArgumentBuiltin ?: boolean
9595 /** An `unset -f` was read, so its arguments name functions and leave variable attributes alone. */
9696 unsetsFunctions ?: boolean
97+ /**
98+ * Names whose attributes this command changes. Bash expands the command's words and heredocs
99+ * before running it, so an expansion inside the command cannot rely on them yet.
100+ */
101+ declaredNames ?: Set < string >
97102 /** True once the command word (past any `name=value` assignment prefix) has been read. */
98103 sawCommandWord : boolean
99104 /** A non-arithmetic assignment prefix is being read, so a later keyword must not mark its value. */
@@ -799,8 +804,18 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
799804 for ( const ended of closed . endedCommands ?? [ ] ) enclosingCommand . nested . push ( ended )
800805 if ( closed . command ) enclosingCommand . nested . push ( closed . command )
801806 }
807+ /**
808+ * Whether a command still being read, below the innermost `ownFrames` frames, changes `name`'s
809+ * attributes. An expansion inside that command runs before the change, so its attribute is
810+ * unknown there and lookups take the arithmetic side: integer, indexed.
811+ */
812+ const declaredByEnclosingCommand = ( name : string , ownFrames = 1 ) =>
813+ frames
814+ . slice ( 0 , frames . length - ownFrames )
815+ . some ( ( frame ) => frame . command ?. declaredNames ?. has ( name ) )
802816 /** Whether `name` has the integer attribute here: the nearest scope that sets or clears it wins. */
803817 const declaresInteger = ( name : string ) : boolean => {
818+ if ( declaredByEnclosingCommand ( name ) ) return true
804819 for ( let depth = frames . length - 1 ; depth >= 0 ; depth -= 1 ) {
805820 const attribute = frames [ depth ] . integerAttribute ?. get ( name )
806821 if ( attribute !== undefined ) return attribute
@@ -816,14 +831,22 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
816831 }
817832 for ( const name of frame . associativeArrays ?? [ ] ) scope . associativeArrays . add ( name )
818833 }
834+ // A heredoc body expands before any command still being read runs, its receiver included.
835+ for ( const frame of frames ) {
836+ for ( const name of frame . command ?. declaredNames ?? [ ] ) {
837+ scope . integerAttribute . set ( name , true )
838+ scope . associativeArrays . delete ( name )
839+ }
840+ }
819841 return scope
820842 }
821843 /** Whether `name` is a `declare -A` associative array, whose subscript is a string key, not arithmetic. */
822- const isAssociativeArray = ( name : string ) =>
844+ const isAssociativeArray = ( name : string , ownFrames = 1 ) =>
845+ ! declaredByEnclosingCommand ( name , ownFrames ) &&
823846 frames . some ( ( frame ) => frame . associativeArrays ?. has ( name ) )
824847 /** A subscript frame for `name`'s array: a string key for an associative array, else an arithmetic index. */
825- const subscriptFrameFor = ( name : string | undefined ) =>
826- subscriptFrame ( isAssociativeArray ( name ?? '' ) ? 'keysubscript' : 'arithmetic' )
848+ const subscriptFrameFor = ( name : string | undefined , ownFrames = 1 ) =>
849+ subscriptFrame ( isAssociativeArray ( name ?? '' , ownFrames ) ? 'keysubscript' : 'arithmetic' )
827850 /** Forget a name's tracked array type in every scope — a re-declaration as `-a`/`-A` resets it. */
828851 const clearArrayType = ( name : string ) => {
829852 for ( const frame of frames ) frame . associativeArrays ?. delete ( name )
@@ -968,6 +991,9 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
968991 // A re-declaration resets the name's array type before this one's attributes apply, so a
969992 // later `declare -a` (indexed) clears an earlier `-A` (associative) and vice versa. The
970993 // integer attribute survives it — only `+i` or `unset` removes that.
994+ if ( command . associativeOption || command . indexedOption || command . integerOption ) {
995+ ; ( command . declaredNames ??= new Set ( ) ) . add ( declared )
996+ }
971997 if ( command . associativeOption || command . indexedOption ) clearArrayType ( declared )
972998 if ( command . associativeOption ) ( frame . associativeArrays ??= new Set ( ) ) . add ( declared )
973999 if ( command . integerOption ) {
@@ -981,7 +1007,10 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
9811007 if ( command . nameArgumentBuiltin && command . sawCommandWord ) {
9821008 const option = DECLARATION_OPTION . exec ( word )
9831009 if ( option ?. [ 1 ] === '-' && option [ 2 ] . includes ( 'f' ) ) command . unsetsFunctions = true
984- else if ( ! command . unsetsFunctions && SHELL_BARE_NAME . test ( word ) ) clearNameType ( word )
1010+ else if ( ! command . unsetsFunctions && SHELL_BARE_NAME . test ( word ) ) {
1011+ ; ( command . declaredNames ??= new Set ( ) ) . add ( word )
1012+ clearNameType ( word )
1013+ }
9851014 }
9861015 // An assignment is one only in command-prefix or declaration-argument position; `echo n=1` or
9871016 // `printf a[i]=1` passes an ordinary string that bash never evaluates.
@@ -1298,7 +1327,8 @@ function collectShellOccurrenceContexts<T extends ShellSpan>(
12981327 / [ \s ( ] / . test ( code [ index - 1 ] ?? ' ' ) &&
12991328 opensIndexedAssignment ( code , index , end )
13001329 ) {
1301- pushFrame ( subscriptFrameFor ( frame . arrayName ) )
1330+ // The literal belongs to the assignment in the frame below, whose own declaration applies.
1331+ pushFrame ( subscriptFrameFor ( frame . arrayName , 2 ) )
13021332 index += 1
13031333 continue
13041334 }
0 commit comments