Skip to content

Commit 615d64a

Browse files
authored
Merge pull request #1037 from Shopify/find-and-fix-multiple-echo-values
Find/fix multiple `echo` values
2 parents 5c0008c + 0bfc785 commit 615d64a

File tree

7 files changed

+180
-6
lines changed

7 files changed

+180
-6
lines changed

.changeset/yellow-pears-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/theme-check-common': patch
3+
---
4+
5+
Find/fix multiple `echo` values

packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ vi.mock('@shopify/liquid-html-parser', async (importOriginal) => {
1616
};
1717
});
1818

19-
describe('detectTrailingAssignValue', async () => {
19+
describe('detectMultipleAssignValues', async () => {
2020
it('should not report when there are no trailing values', async () => {
2121
const testCases = [`{% assign foo = '123' %}`, `{% assign foo = '123' | upcase %}`];
2222

packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/MultipleAssignValues.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { LiquidTag } from '@shopify/liquid-html-parser';
22
import { Problem, SourceCodeType } from '../../..';
3-
import { ensureValidAst, INVALID_SYNTAX_MESSAGE } from './utils';
3+
import { ensureValidAst, getFirstValueInMarkup, INVALID_SYNTAX_MESSAGE } from './utils';
44

55
export function detectMultipleAssignValues(
66
node: LiquidTag,
@@ -42,9 +42,7 @@ export function detectMultipleAssignValues(
4242
assignmentValue,
4343
] = match;
4444

45-
// Only capture the first item in the value section
46-
const firstValueMatch = assignmentValue.match(/"[^"]*"|'[^']*'|\S+/);
47-
const firstAssignmentValue = firstValueMatch ? firstValueMatch[0] : null;
45+
const firstAssignmentValue = getFirstValueInMarkup(assignmentValue);
4846

4947
if (!firstAssignmentValue) {
5048
return;
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { expect, describe, it, vi } from 'vitest';
2+
import { applyFix, runLiquidCheck } from '../../../test';
3+
import { LiquidHTMLSyntaxError } from '../index';
4+
import { toLiquidAST } from '@shopify/liquid-html-parser';
5+
6+
vi.mock('@shopify/liquid-html-parser', async (importOriginal) => {
7+
const original: any = await importOriginal();
8+
9+
return {
10+
...original,
11+
12+
// we will be mocked later on
13+
toLiquidAST: vi.fn().mockImplementation((source, options) => {
14+
return original.toLiquidAST(source, options);
15+
}),
16+
};
17+
});
18+
19+
describe('detectMultipleEchoValues', async () => {
20+
it('should not report when there are no trailing values', async () => {
21+
const testCases = [
22+
`{% echo '123' %}`,
23+
`{% echo '123' | upcase %}`,
24+
`{{ '123' }}`,
25+
`{{ '123' | upcase }}`,
26+
];
27+
28+
for (const sourceCode of testCases) {
29+
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
30+
expect(offenses).to.have.length(0);
31+
}
32+
});
33+
34+
it('should report when there are multiple values (no filters)', async () => {
35+
const testCases = [
36+
[`{% echo '123' 555 text %}`, "{% echo '123' %}"],
37+
[`{% echo "123" 555 text %}`, '{% echo "123" %}'],
38+
[`{% echo 123 555 text %}`, '{% echo 123 %}'],
39+
[`{% echo true 555 text %}`, '{% echo true %}'],
40+
[`{{ '123' 555 text }}`, `{{ '123' }}`],
41+
[`{{ "123" 555 text }}`, `{{ "123" }}`],
42+
[`{{ 123 555 text }}`, `{{ 123 }}`],
43+
[`{{ true 555 text }}`, `{{ true }}`],
44+
];
45+
46+
for (const [sourceCode, expected] of testCases) {
47+
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
48+
expect(offenses).to.have.length(1);
49+
expect(offenses[0].message).to.equal('Syntax is not supported');
50+
51+
const fixed = applyFix(sourceCode, offenses[0]);
52+
expect(fixed).to.equal(expected);
53+
}
54+
});
55+
56+
it('should report when there are multiple values (with filters)', async () => {
57+
const testCases = [
58+
[`{% echo '123' 555 text | upcase %}`, "{% echo '123' | upcase %}"],
59+
[`{% echo "123" 555 text | upcase %}`, '{% echo "123" | upcase %}'],
60+
[`{% echo 123 555 text | default: 0 %}`, '{% echo 123 | default: 0 %}'],
61+
[`{% echo true 555 text | fake-filter: 'yes' %}`, "{% echo true | fake-filter: 'yes' %}"],
62+
[`{{ '123' 555 text | upcase }}`, `{{ '123' | upcase }}`],
63+
[`{{ "123" 555 text | default: 0 }}`, `{{ "123" | default: 0 }}`],
64+
[`{{ 123 555 text | fake-filter: 'yes' }}`, `{{ 123 | fake-filter: 'yes' }}`],
65+
[`{{ true 555 text | fake-filter: 'yes' }}`, `{{ true | fake-filter: 'yes' }}`],
66+
];
67+
68+
for (const [sourceCode, expected] of testCases) {
69+
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
70+
expect(offenses).to.have.length(1);
71+
expect(offenses[0].message).to.equal('Syntax is not supported');
72+
73+
const fixed = applyFix(sourceCode, offenses[0]);
74+
expect(fixed).to.equal(expected);
75+
}
76+
});
77+
78+
it('should not report when the fixed code produces an invalid AST', async () => {
79+
const sourceCode = `{% echo '123' 555 text %}`;
80+
81+
vi.mocked(toLiquidAST).mockImplementation((_source, _options) => {
82+
throw new SyntaxError('Invalid AST');
83+
});
84+
85+
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
86+
expect(offenses).to.have.length(0);
87+
});
88+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { LiquidTag, LiquidVariableOutput, NodeTypes } from '@shopify/liquid-html-parser';
2+
import { Problem, SourceCodeType } from '../../..';
3+
import { ensureValidAst, getFirstValueInMarkup, INVALID_SYNTAX_MESSAGE } from './utils';
4+
5+
export function detectMultipleEchoValues(
6+
node: LiquidTag | LiquidVariableOutput,
7+
): Problem<SourceCodeType.LiquidHtml> | undefined {
8+
// We've broken it up into two groups:
9+
// 1. The variable(s)
10+
// 2. The filter section (non-captured)
11+
const ECHO_MARKUP_REGEX = /([^|]*)(?:\s*\|\s*.*)?$/m;
12+
13+
if (node.type === NodeTypes.LiquidTag && node.name !== 'echo') {
14+
return;
15+
}
16+
17+
const markup = node.markup;
18+
19+
if (typeof markup !== 'string') {
20+
return;
21+
}
22+
23+
const match = markup.match(ECHO_MARKUP_REGEX);
24+
if (!match) {
25+
return;
26+
}
27+
28+
const [, echoValue] = match;
29+
30+
const firstEchoValue = getFirstValueInMarkup(echoValue);
31+
32+
if (!firstEchoValue) {
33+
return;
34+
}
35+
36+
const removalIndices = (source: string) => {
37+
const offset = source.indexOf(markup);
38+
39+
return {
40+
startIndex: offset + firstEchoValue.length,
41+
endIndex: offset + echoValue.trimEnd().length,
42+
};
43+
};
44+
45+
const tagSource = node.source.slice(node.position.start, node.position.end);
46+
const { startIndex: tagSourceRemovalStartIndex, endIndex: tagSourceRemovalEndIndex } =
47+
removalIndices(tagSource);
48+
49+
if (
50+
!ensureValidAst(
51+
tagSource.slice(0, tagSourceRemovalStartIndex) + tagSource.slice(tagSourceRemovalEndIndex),
52+
)
53+
) {
54+
// If the new AST is invalid, we don't want to auto-fix it
55+
return;
56+
}
57+
58+
const { startIndex, endIndex } = removalIndices(node.source);
59+
60+
return {
61+
message: INVALID_SYNTAX_MESSAGE,
62+
startIndex,
63+
endIndex,
64+
fix: (corrector) => {
65+
corrector.replace(startIndex, endIndex, '');
66+
},
67+
};
68+
}

packages/theme-check-common/src/checks/liquid-html-syntax-error/checks/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,8 @@ export function ensureValidAst(source: string) {
1111
return false;
1212
}
1313
}
14+
15+
export function getFirstValueInMarkup(markup: string) {
16+
const match = markup.match(/"[^"]*"|'[^']*'|\S+/);
17+
return match?.at(0);
18+
}

packages/theme-check-common/src/checks/liquid-html-syntax-error/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Severity, SourceCodeType, LiquidCheckDefinition } from '../../types';
22
import { getOffset, isError } from '../../utils';
33
import { detectMultipleAssignValues } from './checks/MultipleAssignValues';
44
import { detectInvalidBooleanExpressions } from './checks/InvalidBooleanExpressions';
5+
import { detectMultipleEchoValues as detectMultipleEchoValues } from './checks/MultipleEchoValues';
56

67
type LineColPosition = {
78
line: number;
@@ -49,7 +50,16 @@ export const LiquidHTMLSyntaxError: LiquidCheckDefinition = {
4950
context.report(problem);
5051
},
5152
async LiquidTag(node) {
52-
const problem = detectMultipleAssignValues(node);
53+
const problem = detectMultipleAssignValues(node) || detectMultipleEchoValues(node);
54+
55+
if (!problem) {
56+
return;
57+
}
58+
59+
context.report(problem);
60+
},
61+
async LiquidVariableOutput(node) {
62+
const problem = detectMultipleEchoValues(node);
5363

5464
if (!problem) {
5565
return;

0 commit comments

Comments
 (0)