@@ -26,13 +26,19 @@ interface HeredocDeclaration {
2626type ShellQuote = 'none' | 'single' | 'double' | 'ansi'
2727
2828interface ShellScanFrame {
29- kind : 'root' | 'command' | 'arithmetic' | 'backtick' | 'array'
29+ kind : 'root' | 'command' | 'arithmetic' | 'backtick' | 'array' | 'conditional'
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
33+ /** Some operands become arithmetic only after a following assignment or comparison operator . */
34+ arithmeticEnabled ?: boolean
3535 arithmeticParent ?: ShellScanFrame
36+ conditional ?: {
37+ parent ?: ShellScanFrame
38+ previousOperand ?: ShellScanFrame
39+ word ?: ShellScanFrame
40+ numericOperand : boolean
41+ }
3642 wordStart ?: number
3743 commandPosition ?: boolean
3844 declarationCommand ?: boolean
@@ -142,6 +148,7 @@ function readShellArrayStart(
142148 const assignment =
143149 parent . quote === 'none' &&
144150 ! parent . literalRoot &&
151+ parent . kind !== 'conditional' &&
145152 wordStart &&
146153 ( parent . kind === 'array' || parent . commandPosition !== false || parent . declarationCommand ) &&
147154 ( hasName || ( character === '[' && parent . kind === 'array' ) )
@@ -152,7 +159,7 @@ function readShellArrayStart(
152159 quote : 'none' ,
153160 parenthesisDepth : character === '(' ? 1 : 0 ,
154161 ...( character === '['
155- ? { bracketDepth : 1 , ...( ! parameter ? { arrayAssignment : false } : { } ) }
162+ ? { bracketDepth : 1 , ...( ! parameter ? { arithmeticEnabled : false } : { } ) }
156163 : { } ) ,
157164 literalRoot : false ,
158165 }
@@ -333,6 +340,52 @@ function trackShellCommandPosition(code: string, index: number, frame: ShellScan
333340 }
334341}
335342
343+ /** Keep operand contexts pending until the following conditional operator is known. */
344+ function trackShellConditionalOperand ( code : string , index : number , frame : ShellScanFrame ) : void {
345+ const conditional = frame . conditional
346+ if ( ! conditional || frame . quote !== 'none' ) return
347+ const character = code [ index ]
348+ const closing =
349+ frame . wordStart === undefined &&
350+ code . startsWith ( ']]' , index ) &&
351+ ( index + 2 === code . length || / \s | [ ; & | ( ) < > ] / . test ( code [ index + 2 ] ) )
352+ const separator = / [ & | ( ) ] / . test ( character ) || closing
353+ if ( ! separator && ! / \s / . test ( character ) ) {
354+ if ( character === '\\' && / ^ \r ? \n / . test ( code . slice ( index + 1 , index + 3 ) ) ) return
355+ if ( frame . wordStart === undefined ) {
356+ frame . wordStart = index
357+ conditional . word = {
358+ kind : 'arithmetic' ,
359+ quote : 'none' ,
360+ parenthesisDepth : 0 ,
361+ literalRoot : false ,
362+ arithmeticEnabled : conditional . numericOperand ,
363+ arithmeticParent : conditional . parent ,
364+ }
365+ conditional . numericOperand = false
366+ frame . arithmeticParent = conditional . word
367+ }
368+ return
369+ }
370+
371+ if ( frame . wordStart !== undefined ) {
372+ const word = code . slice ( frame . wordStart , index ) . replace ( / \\ \r ? \n / g, '' )
373+ if ( / ^ - (?: e q | n e | l t | l e | g t | g e ) $ / . test ( word ) ) {
374+ if ( conditional . previousOperand ) conditional . previousOperand . arithmeticEnabled = true
375+ conditional . numericOperand = true
376+ } else {
377+ conditional . previousOperand = conditional . word
378+ }
379+ frame . wordStart = undefined
380+ conditional . word = undefined
381+ frame . arithmeticParent = conditional . parent
382+ }
383+ if ( separator ) {
384+ conditional . previousOperand = undefined
385+ conditional . numericOperand = false
386+ }
387+ }
388+
336389function parseHeredocHeaders (
337390 code : string ,
338391 lineStart : number ,
@@ -542,7 +595,7 @@ function getUnsupportedShellPosition(
542595 context : ShellOccurrenceContext
543596) : string | undefined {
544597 for ( let frame = context . arithmeticFrame ; frame ; frame = frame . arithmeticParent ) {
545- if ( frame . arrayAssignment !== false ) return 'in a shell arithmetic expression'
598+ if ( frame . arithmeticEnabled !== false ) return 'in a shell arithmetic expression'
546599 }
547600 if ( code [ occurrence . start - 1 ] === '$' ) return 'immediately after "$"'
548601 if ( context . quote !== 'none' ) return undefined
@@ -588,6 +641,7 @@ function collectShellOccurrenceContexts(
588641 if ( ! frame ) break
589642
590643 trackShellCommandPosition ( code , index , frame )
644+ trackShellConditionalOperand ( code , index , frame )
591645 const occurrence = occurrenceByStart . get ( index )
592646 if ( occurrence ) {
593647 contexts . set ( occurrence , {
@@ -622,7 +676,10 @@ function collectShellOccurrenceContexts(
622676 const expansion = readShellExpansionStart (
623677 code ,
624678 index ,
625- frame . quote === 'none' && ! frame . literalRoot && frame . kind !== 'arithmetic'
679+ frame . quote === 'none' &&
680+ ! frame . literalRoot &&
681+ frame . kind !== 'arithmetic' &&
682+ frame . kind !== 'conditional'
626683 )
627684 const array = readShellArrayStart ( code , index , frame )
628685 if ( array ) {
@@ -664,6 +721,34 @@ function collectShellOccurrenceContexts(
664721 index += 1
665722 continue
666723 }
724+ if (
725+ frame . kind === 'conditional' &&
726+ frame . wordStart === undefined &&
727+ code . startsWith ( ']]' , index )
728+ ) {
729+ frames . pop ( )
730+ index += 2
731+ continue
732+ }
733+ if (
734+ ! frame . literalRoot &&
735+ frame . commandPosition !== false &&
736+ code . startsWith ( '[[' , index ) &&
737+ ( index === 0 || / \s | [ ; & | ( ) ] / . test ( code [ index - 1 ] ) ) &&
738+ ( index + 2 === end || / \s | [ ( ) ] / . test ( code [ index + 2 ] ) )
739+ ) {
740+ const conditional : ShellScanFrame = {
741+ kind : 'conditional' ,
742+ quote : 'none' ,
743+ parenthesisDepth : 0 ,
744+ literalRoot : false ,
745+ conditional : { numericOperand : false } ,
746+ }
747+ pushShellFrame ( frames , conditional )
748+ if ( conditional . conditional ) conditional . conditional . parent = conditional . arithmeticParent
749+ index += 2
750+ continue
751+ }
667752 if ( ! frame . literalRoot && shellCommentStarts ( code , index ) ) {
668753 const newline = code . indexOf ( '\n' , index )
669754 if ( newline !== - 1 && newline < end ) trackShellCommandPosition ( code , newline , frame )
@@ -715,8 +800,8 @@ function collectShellOccurrenceContexts(
715800 if ( character === ']' ) {
716801 frame . bracketDepth -= 1
717802 if ( frame . bracketDepth === 0 ) {
718- if ( frame . arrayAssignment !== undefined ) {
719- frame . arrayAssignment = code [ index + 1 ] === '=' || code . startsWith ( '+=' , index + 1 )
803+ if ( frame . arithmeticEnabled !== undefined ) {
804+ frame . arithmeticEnabled = code [ index + 1 ] === '=' || code . startsWith ( '+=' , index + 1 )
720805 }
721806 frames . pop ( )
722807 }
0 commit comments