@@ -26,17 +26,22 @@ interface HeredocDeclaration {
2626type ShellQuote = 'none' | 'single' | 'double' | 'ansi'
2727
2828interface ShellScanFrame {
29- kind : 'root' | 'command' | 'arithmetic' | 'backtick'
29+ kind : 'root' | 'command' | 'arithmetic' | 'backtick' | 'array'
3030 quote : ShellQuote
3131 parenthesisDepth : number
3232 bracketDepth ?: number
33+ /** A bare name[index] is arithmetic only when its closing bracket is followed by assignment. */
34+ arrayAssignment ?: boolean
3335 literalRoot : boolean
3436}
3537
3638interface ShellOccurrenceContext {
3739 quote : ShellQuote
38- /** Enclosing arithmetic also re-evaluates the output of a nested command substitution. */
39- arithmetic ?: boolean
40+ /**
41+ * Enclosing arithmetic also re-evaluates nested substitutions. Keep frame references so a
42+ * bare subscript can be classified when its closing bracket confirms an assignment.
43+ */
44+ arithmeticFrames ?: ShellScanFrame [ ]
4045 unsupported ?: 'escaped sequence'
4146}
4247
@@ -98,6 +103,49 @@ function readShellExpansionStart(
98103 }
99104}
100105
106+ function readShellArrayStart (
107+ code : string ,
108+ index : number ,
109+ parent : ShellScanFrame
110+ ) : ShellScanFrame | undefined {
111+ const character = code [ index ]
112+ if ( character !== '[' && character !== '(' ) return undefined
113+
114+ let nameEnd = index
115+ if ( character === '(' ) {
116+ if ( code [ index - 1 ] !== '=' ) return undefined
117+ nameEnd -= 1
118+ if ( code [ nameEnd - 1 ] === '+' ) nameEnd -= 1
119+ }
120+ let nameStart = nameEnd
121+ while ( nameStart > 0 && / [ A - Z a - z 0 - 9 _ ] / . test ( code [ nameStart - 1 ] ) ) nameStart -= 1
122+ const hasName = nameStart < nameEnd && / [ A - Z a - z _ ] / . test ( code [ nameStart ] )
123+ const parameterStart =
124+ code [ nameStart - 1 ] === '#' || code [ nameStart - 1 ] === '!' ? nameStart - 3 : nameStart - 2
125+ const parameter =
126+ character === '[' &&
127+ hasName &&
128+ code . slice ( parameterStart , parameterStart + 2 ) === '${' &&
129+ ! isBackslashEscaped ( code , parameterStart )
130+ const wordStart = nameStart === 0 || / \s | [ ; & | ( ) ] / . test ( code [ nameStart - 1 ] )
131+ const assignment =
132+ parent . quote === 'none' &&
133+ ! parent . literalRoot &&
134+ wordStart &&
135+ ( hasName || ( character === '[' && parent . kind === 'array' ) )
136+ if ( ! parameter && ! assignment ) return undefined
137+
138+ return {
139+ kind : character === '[' ? 'arithmetic' : 'array' ,
140+ quote : 'none' ,
141+ parenthesisDepth : character === '(' ? 1 : 0 ,
142+ ...( character === '['
143+ ? { bracketDepth : 1 , ...( ! parameter ? { arrayAssignment : false } : { } ) }
144+ : { } ) ,
145+ literalRoot : false ,
146+ }
147+ }
148+
101149function decodeAnsiCCharacter ( code : string , index : number ) : { value : string ; end : number } {
102150 const character = code [ index ]
103151 const simple : Record < string , string > = {
@@ -435,7 +483,9 @@ function getUnsupportedShellPosition(
435483 occurrence : CodePlaceholderOccurrence ,
436484 context : ShellOccurrenceContext
437485) : string | undefined {
438- if ( context . arithmetic ) return 'in a shell arithmetic expression'
486+ if ( context . arithmeticFrames ?. some ( ( frame ) => frame . arrayAssignment !== false ) ) {
487+ return 'in a shell arithmetic expression'
488+ }
439489 if ( code [ occurrence . start - 1 ] === '$' ) return 'immediately after "$"'
440490 if ( context . quote !== 'none' ) return undefined
441491
@@ -483,7 +533,7 @@ function collectShellOccurrenceContexts(
483533 if ( occurrence ) {
484534 contexts . set ( occurrence , {
485535 quote : frame . quote ,
486- arithmetic : frames . some ( ( candidate ) => candidate . kind === 'arithmetic' ) ,
536+ arithmeticFrames : frames . filter ( ( candidate ) => candidate . kind === 'arithmetic' ) ,
487537 } )
488538 index = occurrence . end
489539 continue
@@ -515,6 +565,12 @@ function collectShellOccurrenceContexts(
515565 index ,
516566 frame . quote === 'none' && ! frame . literalRoot && frame . kind !== 'arithmetic'
517567 )
568+ const array = readShellArrayStart ( code , index , frame )
569+ if ( array ) {
570+ frames . push ( array )
571+ index += 1
572+ continue
573+ }
518574 if ( frame . quote === 'double' ) {
519575 if ( character === '\\' ) {
520576 const escaped = occurrenceByStart . get ( index + 1 )
@@ -598,17 +654,28 @@ function collectShellOccurrenceContexts(
598654 if ( character === '[' ) frame . bracketDepth += 1
599655 if ( character === ']' ) {
600656 frame . bracketDepth -= 1
601- if ( frame . bracketDepth === 0 ) frames . pop ( )
657+ if ( frame . bracketDepth === 0 ) {
658+ if ( frame . arrayAssignment !== undefined ) {
659+ frame . arrayAssignment = code [ index + 1 ] === '=' || code . startsWith ( '+=' , index + 1 )
660+ }
661+ frames . pop ( )
662+ }
602663 }
603664 index += 1
604665 continue
605666 }
606- if ( ( frame . kind === 'command' || frame . kind === 'arithmetic' ) && character === '(' ) {
667+ if (
668+ ( frame . kind === 'command' || frame . kind === 'arithmetic' || frame . kind === 'array' ) &&
669+ character === '('
670+ ) {
607671 frame . parenthesisDepth += 1
608672 index += 1
609673 continue
610674 }
611- if ( ( frame . kind === 'command' || frame . kind === 'arithmetic' ) && character === ')' ) {
675+ if (
676+ ( frame . kind === 'command' || frame . kind === 'arithmetic' || frame . kind === 'array' ) &&
677+ character === ')'
678+ ) {
612679 frame . parenthesisDepth -= 1
613680 if ( frame . parenthesisDepth === 0 ) frames . pop ( )
614681 index += 1
0 commit comments