diff --git a/.changeset/fix-dot-lookup-digit-property.md b/.changeset/fix-dot-lookup-digit-property.md new file mode 100644 index 000000000..042963d59 --- /dev/null +++ b/.changeset/fix-dot-lookup-digit-property.md @@ -0,0 +1,5 @@ +--- +'@shopify/liquid-html-parser': minor +--- + +Allow property names starting with a digit in dot lookups (e.g. `{{ address.2country.name }}`). This fixes an autocomplete regression in the language server where suggestions were silently dropped after a property whose name started with a number, because the grammar treated the dot lookup as an unparseable token. `variableSegment` (used for variable declarations and filter names) is unchanged and still rejects leading digits. diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index c60759c29..76ab371c8 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -285,7 +285,12 @@ Liquid <: Helpers { | indexLookup | dotLookup indexLookup = space* "[" space* liquidExpression space* "]" - dotLookup = space* "." space* identifier + dotLookup = space* "." space* propertyName + // Property names accessed via dot lookup can start with a digit + // (e.g. `settings.2country`). This differs from `variableSegment`, which + // is used for variable declarations and filter names where leading + // digits are not allowed. + propertyName = (letter | "_" | digit) (~endOfTagName identifierCharacter)* "?"? liquidFilter = space* "|" space* identifier (space* ":" space* arguments (space* ",")?)? @@ -583,6 +588,7 @@ WithPlaceholderLiquid <: Liquid { snippetExpression renderVariableExpression? renderAliasExpression? completionModeRenderArguments liquidTagName := (letter | "█") (alnum | "_")* variableSegment := (letter | "_" | "█") (identifierCharacter | "█")* + propertyName := (letter | "_" | digit | "█") (identifierCharacter | "█")* "?"? liquidDoc := liquidDocStart liquidDocBody @@ -597,6 +603,7 @@ WithPlaceholderLiquidStatement <: LiquidStatement { snippetExpression renderVariableExpression? renderAliasExpression? completionModeRenderArguments liquidTagName := (letter | "█") (alnum | "_")* variableSegment := (letter | "_" | "█") (identifierCharacter | "█")* + propertyName := (letter | "_" | digit | "█") (identifierCharacter | "█")* "?"? liquidDoc := liquidDocStart liquidDocBody @@ -611,6 +618,7 @@ WithPlaceholderLiquidHTML <: LiquidHTML { snippetExpression renderVariableExpression? renderAliasExpression? completionModeRenderArguments liquidTagName := (letter | "█") (alnum | "_")* variableSegment := (letter | "_" | "█") (identifierCharacter | "█")* + propertyName := (letter | "_" | digit | "█") (identifierCharacter | "█")* "?"? leadingTagNameTextNode := (letter | "█") (alnum | "-" | ":" | "█")* trailingTagNameTextNode := (alnum | "-" | ":" | "█")+ liquidDoc := diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index c5b20f1e8..778d4e240 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -174,6 +174,9 @@ describe('Unit: Stage 1 (CST)', () => { { expression: `x["y"].z`, name: 'x', lookups: ['y', 'z'] }, { expression: `["product"]`, name: null, lookups: ['product'] }, { expression: `page.about-us`, name: 'page', lookups: ['about-us'] }, + // Property names (dot lookup targets) can start with a digit + { expression: `address.2country`, name: 'address', lookups: ['2country'] }, + { expression: `address.2country.name`, name: 'address', lookups: ['2country', 'name'] }, { expression: `["x"].y`, name: null, lookups: ['x', 'y'] }, { expression: `["x"]["y"]`, name: null, lookups: ['x', 'y'] }, { expression: `x[y]`, name: 'x', lookups: [v('y')] }, @@ -1882,6 +1885,17 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.markup.expression.lookups.0.type').to.eql('VariableLookup'); expectPath(cst, '0.markup.expression.lookups.0.name').to.eql('█'); + // Completion after a property whose name starts with a digit + cst = toCST('{{ address.2country.█ }}'); + expectPath(cst, '0.type').to.eql('LiquidVariableOutput'); + expectPath(cst, '0.markup.type').to.eql('LiquidVariable'); + expectPath(cst, '0.markup.expression.type').to.eql('VariableLookup'); + expectPath(cst, '0.markup.expression.name').to.eql('address'); + expectPath(cst, '0.markup.expression.lookups.0.type').to.eql('String'); + expectPath(cst, '0.markup.expression.lookups.0.value').to.eql('2country'); + expectPath(cst, '0.markup.expression.lookups.1.type').to.eql('String'); + expectPath(cst, '0.markup.expression.lookups.1.value').to.eql('█'); + cst = toCST('{% echo █ %}'); expectPath(cst, '0.type').to.eql('LiquidTag'); expectPath(cst, '0.markup.type').to.eql('LiquidVariable');