Skip to content

Commit dd1f4c8

Browse files
authored
Fix false reporting of UndefinedObject within visible_if (#1061)
We were visiting those nodes as though they were part of the execution of the file when in fact those are not parsed by the backend but stripped away. I first tried doing that at the parsing layer by putting the body as a TextNode (the same way `{% raw %}` does it) but quickly discovered that we do depend on it being parsed for the StaticStylesheetAndJavascriptTags check. So instead, I just added a helper that both UndefinedObject and UnusedAssign use to skip visiting of nodes that are parent'ed by a "raw-like" tag Fixes #685
1 parent 4bcb112 commit dd1f4c8

File tree

10 files changed

+99
-19
lines changed

10 files changed

+99
-19
lines changed

.changeset/yellow-needles-turn.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+
Fix false-positive reporting of UndefinedObject within {% schema %} tags

packages/liquid-html-parser/src/stage-1-cst.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,8 @@ function toCST<T>(
620620
const nameNode = tokens[3];
621621
const rawMarkupStringNode = tokens[9];
622622
switch (nameNode.sourceString) {
623+
// {% schema %} parses its content as a string and should not be visited
624+
case 'schema':
623625
// {% raw %} accepts syntax errors, we shouldn't try to parse that
624626
case 'raw': {
625627
return toCST(
@@ -632,7 +634,10 @@ function toCST<T>(
632634
);
633635
}
634636

635-
// {% javascript %}, {% style %}
637+
// {% style %} actually parses its child nodes, so they are part of the AST
638+
// {% javascript %}, {% stylesheet %} don't, but we want to flag folks that
639+
// those are not supported in StaticStylesheetAndJavascriptTags, so we put
640+
// them in the AST
636641
default: {
637642
return toCST(
638643
source,

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
import { LiquidBooleanExpression, LiquidHtmlNode, NodeTypes } from '@shopify/liquid-html-parser';
22
import { Problem, SourceCodeType } from '../../..';
33
import { INVALID_SYNTAX_MESSAGE } from './utils';
4+
import { isWithinRawTagThatDoesNotParseItsContents } from '../../utils';
45

56
export function detectInvalidBooleanExpressions(
67
node: LiquidBooleanExpression,
78
ancestors: LiquidHtmlNode[],
89
): Problem<SourceCodeType.LiquidHtml> | undefined {
9-
const condition = node.condition;
10-
11-
// We are okay with boolean expressions in schema tags (specifically for visible_if)
12-
if (
13-
ancestors.some(
14-
(ancestor) => ancestor.type === NodeTypes.LiquidRawTag && ancestor.name === 'schema',
15-
)
16-
) {
17-
return;
18-
}
10+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
1911

12+
const condition = node.condition;
2013
if (condition.type !== NodeTypes.Comparison && condition.type !== NodeTypes.LogicalExpression) {
2114
return;
2215
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ describe('Module: LiquidHTMLSyntaxError', () => {
9595
{% if some_variable %}
9696
Hello, world!
9797
{% endif %}
98+
{% schema %}
99+
{
100+
"settings": [{ "visible_if": "{{ foo.bar == true }}" }]
101+
}
102+
{% endschema %}
98103
`;
99104

100105
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { detectInvalidLoopArguments } from './checks/InvalidLoopArguments';
99
import { detectConditionalNodeUnsupportedParenthesis } from './checks/InvalidConditionalNodeParenthesis';
1010
import { detectInvalidFilterName } from './checks/InvalidFilterName';
1111
import { detectInvalidPipeSyntax } from './checks/InvalidPipeSyntax';
12+
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
1213

1314
type LineColPosition = {
1415
line: number;
@@ -50,6 +51,8 @@ export const LiquidHTMLSyntaxError: LiquidCheckDefinition = {
5051
if (!isError(ast)) {
5152
return {
5253
async BooleanExpression(node, ancestors) {
54+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
55+
5356
const problem = detectInvalidBooleanExpressions(node, ancestors);
5457

5558
if (!problem) {
@@ -58,7 +61,9 @@ export const LiquidHTMLSyntaxError: LiquidCheckDefinition = {
5861

5962
context.report(problem);
6063
},
61-
async LiquidTag(node) {
64+
async LiquidTag(node, ancestors) {
65+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
66+
6267
const problems = [
6368
detectMultipleAssignValues(node),
6469
detectInvalidEchoValue(node),
@@ -87,7 +92,10 @@ export const LiquidHTMLSyntaxError: LiquidCheckDefinition = {
8792
pipeProblems.forEach((pipeProblem) => context.report(pipeProblem));
8893
}
8994
},
90-
async LiquidBranch(node) {
95+
96+
async LiquidBranch(node, ancestors) {
97+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
98+
9199
const problem = detectInvalidConditionalNode(node);
92100

93101
if (!problem) {
@@ -96,7 +104,10 @@ export const LiquidHTMLSyntaxError: LiquidCheckDefinition = {
96104

97105
context.report(problem);
98106
},
99-
async LiquidVariableOutput(node) {
107+
108+
async LiquidVariableOutput(node, ancestors) {
109+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
110+
100111
const filterProblems = await detectInvalidFilterName(node, await filtersPromise);
101112
if (filterProblems.length > 0) {
102113
filterProblems.forEach((problem) => context.report(problem));

packages/theme-check-common/src/checks/undefined-object/index.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, describe, it } from 'vitest';
1+
import { expect, describe, it, assert } from 'vitest';
22
import { UndefinedObject } from './index';
33
import { runLiquidCheck, highlightedOffenses } from '../../test';
44
import { Offense } from '../../types';
@@ -432,4 +432,31 @@ describe('Module: UndefinedObject', () => {
432432
expect(offenses).toHaveLength(1);
433433
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
434434
});
435+
436+
it('should not report an offense when using variables inside visible_if statements in a schema tag', async () => {
437+
const sourceCode = `
438+
{% schema %}
439+
{
440+
"settings": [
441+
{
442+
"type": "text",
443+
"label": "foo",
444+
"id": "foo",
445+
}
446+
{
447+
"type": "text",
448+
"label": "bar",
449+
"id": "bar",
450+
"visible_if": "{{ block.settings.foo }}",
451+
}
452+
]
453+
}
454+
{% endschema %}
455+
`;
456+
457+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
458+
459+
assert(offenses.length == 0);
460+
expect(offenses).to.be.empty;
461+
});
435462
});

packages/theme-check-common/src/checks/undefined-object/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { LiquidCheckDefinition, Severity, SourceCodeType, ThemeDocset } from '../../types';
1717
import { isError, last } from '../../utils';
1818
import { hasLiquidDoc } from '../../liquid-doc/liquidDoc';
19+
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
1920

2021
type Scope = { start?: number; end?: number };
2122

@@ -72,7 +73,9 @@ export const UndefinedObject: LiquidCheckDefinition = {
7273
}
7374
},
7475

75-
async LiquidTag(node) {
76+
async LiquidTag(node, ancestors) {
77+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
78+
7679
if (isLiquidTagAssign(node)) {
7780
indexVariableScope(node.markup.name, {
7881
start: node.blockStartPosition.end,
@@ -134,8 +137,9 @@ export const UndefinedObject: LiquidCheckDefinition = {
134137
},
135138

136139
async VariableLookup(node, ancestors) {
137-
const parent = last(ancestors);
140+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
138141

142+
const parent = last(ancestors);
139143
if (isLiquidTag(parent) && isLiquidTagCapture(parent)) return;
140144

141145
variables.push(node);

packages/theme-check-common/src/checks/unused-assign/index.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('Module: UnusedAssign', () => {
8585
});
8686

8787
it('should not report unused assigns for things used in a raw-like tag', async () => {
88-
const tags = ['style', 'javascript', 'stylesheet'];
88+
const tags = ['style'];
8989
for (const tag of tags) {
9090
const sourceCode = `
9191
{% assign usedVar = 1 %}
@@ -99,6 +99,21 @@ describe('Module: UnusedAssign', () => {
9999
}
100100
});
101101

102+
it('should report unused assigns for things used in raw code that gets stripped away (schema, etc)', async () => {
103+
const tags = ['schema', 'javascript', 'stylesheet'];
104+
for (const tag of tags) {
105+
const sourceCode = `
106+
{% assign usedVar = 1 %}
107+
{% ${tag} %}
108+
{{ usedVar }}
109+
{% end${tag} %}
110+
`;
111+
112+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
113+
expect(offenses).to.not.be.empty;
114+
}
115+
});
116+
102117
it('should not report unused assigns for things used in a HTML raw-like tag', async () => {
103118
const tags = ['style', 'script'];
104119
for (const tag of tags) {

packages/theme-check-common/src/checks/unused-assign/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
NodeTypes,
77
} from '@shopify/liquid-html-parser';
88
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
9+
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
910

1011
export const UnusedAssign: LiquidCheckDefinition = {
1112
meta: {
@@ -34,7 +35,8 @@ export const UnusedAssign: LiquidCheckDefinition = {
3435
}
3536

3637
return {
37-
async LiquidTag(node) {
38+
async LiquidTag(node, ancestors) {
39+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
3840
if (isLiquidTagAssign(node)) {
3941
assignedVariables.set(node.markup.name, node);
4042
} else if (isLiquidTagCapture(node) && node.markup.name) {
@@ -43,6 +45,7 @@ export const UnusedAssign: LiquidCheckDefinition = {
4345
},
4446

4547
async VariableLookup(node, ancestors) {
48+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
4649
const parentNode = ancestors.at(-1);
4750
if (parentNode && isLiquidTagCapture(parentNode)) {
4851
return;

packages/theme-check-common/src/checks/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,15 @@ export function isLoopScopedVariable(variableName: string, ancestors: LiquidHtml
103103
export function isLoopLiquidTag(tag: LiquidTag): tag is LiquidTagFor | LiquidTagTablerow {
104104
return LoopNamedTags.includes(tag.name as any);
105105
}
106+
107+
const RawTagsThatDoNotParseTheirContents = ['raw', 'stylesheet', 'javascript', 'schema'];
108+
109+
function isRawTagThatDoesNotParseItsContent(node: LiquidHtmlNode) {
110+
return (
111+
node.type === NodeTypes.LiquidRawTag && RawTagsThatDoNotParseTheirContents.includes(node.name)
112+
);
113+
}
114+
115+
export function isWithinRawTagThatDoesNotParseItsContents(ancestors: LiquidHtmlNode[]) {
116+
return ancestors.some(isRawTagThatDoesNotParseItsContent);
117+
}

0 commit comments

Comments
 (0)