11import { LiquidBranch , LiquidTag } from '@shopify/liquid-html-parser' ;
2- import { Check , Context , SourceCodeType , Problem } from '../../..' ;
2+ import { SourceCodeType , Problem } from '../../..' ;
3+ import { getValuesInMarkup } from './utils' ;
34
4- const LeftSideEvaluation =
5- / ^ ( [ ' " ] [ ^ ' " ] * [ ' " ] | \d + (?: \. \d + ) ? | t r u e | f a l s e | n i l | e m p t y | b l a n k ) \s + ( (? ! [ a - z A - Z _ ] ) \S .* ) / ;
6- const TrailingJunk = / ^ ( .+ ?(?: = = | ! = | > = | < = | > | < | c o n t a i n s ) \s + \S + ) \s + ( .+ ?) (?: \s + (?: a n d | o r ) \s | $ ) / ;
7- const Comparison = / ^ \s * ( = = | ! = | > = | < = | > | < | c o n t a i n s ) \s / ;
8- const LogicalOperator = / ^ (?: a n d | o r ) \s / ;
5+ type TokenType = 'variable' | keyof typeof TOKEN_PATTERNS ;
6+
7+ interface Token {
8+ value : string ;
9+ type : TokenType ;
10+ }
11+
12+ interface ExpressionIssue {
13+ message : string ;
14+ fix : string ;
15+ }
16+
17+ const TOKEN_PATTERNS = {
18+ logical : / ^ ( a n d | o r ) $ / ,
19+ comparison : / ^ ( = = | ! = | > = | < = | > | < | c o n t a i n s ) $ / ,
20+ invalid : / ^ [ @ # $ & ] $ / ,
21+ literal : / ^ ( [ ' " ] [ ^ ' " ] * [ ' " ] | \d + (?: \. \d + ) ? | t r u e | f a l s e | n i l | e m p t y | b l a n k ) $ / ,
22+ } as const ;
23+
24+ function classifyToken ( value : string ) : TokenType {
25+ for ( const [ type , pattern ] of Object . entries ( TOKEN_PATTERNS ) ) {
26+ if ( pattern . test ( value ) ) {
27+ return type as TokenType ;
28+ }
29+ }
30+ return 'variable' ;
31+ }
932
1033export function detectInvalidConditionalNode (
1134 node : LiquidBranch | LiquidTag ,
@@ -16,7 +39,7 @@ export function detectInvalidConditionalNode(
1639 const markup = node . markup ;
1740 if ( typeof markup !== 'string' || ! markup . trim ( ) ) return ;
1841
19- const issue = checkLaxParsing ( markup ) ;
42+ const issue = analyzeConditionalExpression ( markup ) ;
2043 if ( ! issue ) return ;
2144
2245 const openingTagRange = node . blockStartPosition || node . position ;
@@ -28,7 +51,7 @@ export function detectInvalidConditionalNode(
2851 const endIndex = startIndex + markup . length ;
2952
3053 return {
31- message : `Liquid lax parsing issue: ${ issue . issue } ` ,
54+ message : `Liquid lax parsing issue: ${ issue . message } ` ,
3255 startIndex,
3356 endIndex,
3457 fix : ( corrector ) => {
@@ -37,45 +60,88 @@ export function detectInvalidConditionalNode(
3760 } ;
3861}
3962
40- function checkLaxParsing ( markup : string ) : { issue : string ; fix : string } | null {
41- const trimmed = markup . trim ( ) ;
63+ function isValueToken ( token : Token ) : boolean {
64+ return token . type === 'literal' || token . type === 'variable' ;
65+ }
4266
43- // Pattern 1: Left-side evaluation - parser stops at first complete value
44- // EX: {% if 7 1 > 100 %}hello{% endif %} <-- Lax parsing will ignore everything after 7 so we will remove 1 > 100
45- const leftSideMatch = LeftSideEvaluation . exec ( trimmed ) ;
46- if ( leftSideMatch ) {
47- const [ , leftValue , rest ] = leftSideMatch ;
48- // Check if the remaining string would create a valid comparison or logical operator
49- if ( ! Comparison . test ( rest ) && ! LogicalOperator . test ( rest ) ) {
50- return {
51- issue : `Expression stops at truthy value '${ leftValue } ', ignoring: '${ rest } '` ,
52- fix : leftValue ,
53- } ;
54- }
55- }
67+ function isOperatorToken ( token : Token ) : boolean {
68+ return token . type === 'logical' || token . type === 'comparison' ;
69+ }
5670
57- // Pattern 2: Malformed expression starting with invalid token
58- // EX: {% if 2 %}hello{% endif %} is truthy but any special character such as @#! will evaluate to false
59- const malformedStart = / ^ ( [ ^ a - z A - Z _ ' " \d \s ( ] [ ^ \s ] * ) / . exec ( trimmed ) ;
60- if ( malformedStart ) {
71+ function checkInvalidStartingToken ( tokens : Token [ ] ) : ExpressionIssue | null {
72+ const firstToken = tokens [ 0 ] ;
73+ if ( firstToken . type === 'invalid' || firstToken . type === 'comparison' ) {
6174 return {
62- issue : `Malformed expression starting with invalid token '${ malformedStart [ 1 ] } '` ,
75+ message : `Malformed expression starting with invalid token '${ firstToken . value } '` ,
6376 fix : 'false' ,
6477 } ;
6578 }
79+ return null ;
80+ }
81+
82+ function checkTrailingTokensAfterComparison ( tokens : Token [ ] ) : ExpressionIssue | null {
83+ const COMPARISON_LENGTH = 3 ;
84+ const minTokensForTrailing = COMPARISON_LENGTH + 1 ;
6685
67- // Pattern 3: Trailing junk after complete comparison
68- // EX: {% if 1 > 100 foobar %}hello{% endif %} Lax parsing will ignore everything after 1 > 100 so we will remove foobar
69- const trailingMatch = TrailingJunk . exec ( trimmed ) ;
70- if ( trailingMatch ) {
71- const [ , comparison , trailing ] = trailingMatch ;
72- if ( ! LogicalOperator . test ( trailing ) ) {
73- return {
74- issue : `Trailing tokens ignored after comparison: '${ trailing . trim ( ) } '` ,
75- fix : comparison . trim ( ) ,
76- } ;
86+ for ( let i = 0 ; i <= tokens . length - minTokensForTrailing ; i ++ ) {
87+ const [ v1 , op , v2 ] = tokens . slice ( i , i + 3 ) ;
88+ const remaining = tokens . slice ( i + 3 ) ;
89+
90+ if ( isValueToken ( v1 ) && op . type === 'comparison' && isValueToken ( v2 ) ) {
91+ if ( remaining . length > 0 ) {
92+ if ( remaining [ 0 ] . type !== 'logical' ) {
93+ const validExpr = tokens
94+ . slice ( 0 , i + 3 )
95+ . map ( ( t ) => t . value )
96+ . join ( ' ' ) ;
97+ const junk = remaining . map ( ( t ) => t . value ) . join ( ' ' ) ;
98+ return {
99+ message : `Trailing tokens ignored after comparison: '${ junk . trim ( ) } '` ,
100+ fix : validExpr ,
101+ } ;
102+ }
103+ }
77104 }
78105 }
106+ return null ;
107+ }
108+
109+ function checkLaxParsingIssues ( tokens : Token [ ] ) : ExpressionIssue | null {
110+ for ( let i = 0 ; i < tokens . length - 1 ; i ++ ) {
111+ const current = tokens [ i ] ;
112+ const next = tokens [ i + 1 ] ;
79113
114+ if ( current . type === 'literal' && ! isOperatorToken ( next ) ) {
115+ const remaining = tokens . slice ( i + 1 ) ;
116+ const hasUnknownOperator =
117+ remaining [ 0 ] ?. type === 'variable' && remaining . some ( isOperatorToken ) ;
118+
119+ if ( ! hasUnknownOperator ) {
120+ const ignored = remaining . map ( ( t ) => t . value ) . join ( ' ' ) ;
121+ return {
122+ message : `Expression stops at truthy value '${ current . value } ', ignoring: '${ ignored } '` ,
123+ fix : current . value ,
124+ } ;
125+ }
126+ }
127+ }
80128 return null ;
81129}
130+
131+ function analyzeConditionalExpression ( markup : string ) : ExpressionIssue | null {
132+ const trimmed = markup . trim ( ) ;
133+ if ( ! trimmed ) return null ;
134+
135+ const tokens : Token [ ] = getValuesInMarkup ( trimmed ) . map ( ( { value } ) => ( {
136+ value,
137+ type : classifyToken ( value ) ,
138+ } ) ) ;
139+
140+ if ( tokens . length === 0 ) return null ;
141+
142+ return (
143+ checkInvalidStartingToken ( tokens ) ||
144+ checkTrailingTokensAfterComparison ( tokens ) ||
145+ checkLaxParsingIssues ( tokens )
146+ ) ;
147+ }
0 commit comments