Skip to content

Commit 6935133

Browse files
committed
InvalidComparisonSyntax
1 parent ea9f245 commit 6935133

File tree

5 files changed

+105
-1
lines changed

5 files changed

+105
-1
lines changed

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { VariableName } from './variable-name';
5757
import { AppBlockMissingSchema } from './app-block-missing-schema';
5858
import { UniqueSettingIds } from './unique-settings-id';
5959
import { DuplicateRenderSnippetParams } from './duplicate-render-snippet-params';
60+
import { InvalidComparisonSyntax } from './invalid-comparison-syntax';
6061

6162
export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
6263
AppBlockValidTags,
@@ -70,12 +71,13 @@ export const allChecks: (LiquidCheckDefinition | JSONCheckDefinition)[] = [
7071
CdnPreconnect,
7172
ContentForHeaderModification,
7273
DeprecateBgsizes,
74+
DeprecateLazysizes,
7375
DeprecatedFilter,
7476
DeprecatedTag,
75-
DeprecateLazysizes,
7677
DuplicateRenderSnippetParams,
7778
EmptyBlockContent,
7879
ImgWidthAndHeight,
80+
InvalidComparisonSyntax,
7981
JSONMissingBlock,
8082
JSONSyntaxError,
8183
LiquidFreeSettings,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { expect, describe, it } from 'vitest';
2+
import { highlightedOffenses, runLiquidCheck } from '../../test';
3+
import { InvalidComparisonSyntax } from './index';
4+
5+
describe('Module: InvalidComparisonSyntax', () => {
6+
it('should report an offense an invalid token is used', async () => {
7+
const sourceCode = `
8+
{% if a > b foobar %}
9+
{% endif %}
10+
`;
11+
const offenses = await runLiquidCheck(InvalidComparisonSyntax, sourceCode);
12+
13+
expect(offenses).toHaveLength(1);
14+
expect(offenses[0].message).toEqual(`Invalid token 'foobar' after comparison`);
15+
});
16+
17+
it('should suggest removing the invalid token', async () => {
18+
const sourceCode = `
19+
{% if a > b foobar %}
20+
{% endif %}
21+
`;
22+
const offenses = await runLiquidCheck(InvalidComparisonSyntax, sourceCode);
23+
24+
expect(offenses).toHaveLength(1);
25+
26+
expect(offenses[0]!.suggest![0]!.message).toEqual(`Remove 'foobar'`);
27+
28+
const highlights = highlightedOffenses({ 'file.liquid': sourceCode }, offenses);
29+
expect(highlights).toHaveLength(1);
30+
expect(highlights[0]).toBe('foobar');
31+
});
32+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
2+
3+
export const InvalidComparisonSyntax: LiquidCheckDefinition = {
4+
meta: {
5+
code: 'InvalidComparisonSyntax',
6+
name: 'Invalid syntax after comparison operator',
7+
docs: {
8+
description: 'Ensures comparison operators in Liquid if statements follow valid syntax',
9+
recommended: true,
10+
url: 'https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/invalid-comparison',
11+
},
12+
type: SourceCodeType.LiquidHtml,
13+
severity: Severity.ERROR,
14+
schema: {},
15+
targets: [],
16+
},
17+
18+
create(context) {
19+
return {
20+
async LiquidTag(node) {
21+
if (node.name !== 'if' && node.name !== 'elsif' && node.name !== 'unless') {
22+
return;
23+
}
24+
25+
const markup = node.markup.toString();
26+
27+
const regex =
28+
/(>=|<=|>|<|==|!=)\s+([^\s]+)\s+([^\s]+)(?!\s+(and|or|%}|contains|startswith|endswith))/g;
29+
30+
let match;
31+
while ((match = regex.exec(markup)) !== null) {
32+
const invalidToken = match[3];
33+
34+
if (!isValidComparisonConnector(invalidToken)) {
35+
const invalidTokenOffset = match.index + match[0].lastIndexOf(invalidToken);
36+
37+
const markupStart = node.position.start + node.name.length + 3;
38+
const startIndex = markupStart + invalidTokenOffset + 1;
39+
const endIndex = startIndex + invalidToken.length;
40+
41+
context.report({
42+
message: `Invalid token '${invalidToken}' after comparison`,
43+
startIndex,
44+
endIndex,
45+
suggest: [
46+
{
47+
message: `Remove '${invalidToken}'`,
48+
fix: (corrector) => {
49+
corrector.remove(startIndex, endIndex + 1);
50+
},
51+
},
52+
],
53+
});
54+
}
55+
}
56+
},
57+
};
58+
},
59+
};
60+
61+
function isValidComparisonConnector(token: string): boolean {
62+
const validConnectors = ['and', 'or', '%}', 'contains', 'startswith', 'endswith'];
63+
return validConnectors.some((connector) => token.includes(connector));
64+
}

packages/theme-check-node/configs/all.yml

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ EmptyBlockContent:
6161
ImgWidthAndHeight:
6262
enabled: true
6363
severity: 0
64+
InvalidComparisonSyntax:
65+
enabled: true
66+
severity: 0
6467
JSONMissingBlock:
6568
enabled: true
6669
severity: 0

packages/theme-check-node/configs/recommended.yml

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ EmptyBlockContent:
3939
ImgWidthAndHeight:
4040
enabled: true
4141
severity: 0
42+
InvalidComparisonSyntax:
43+
enabled: true
44+
severity: 0
4245
JSONMissingBlock:
4346
enabled: true
4447
severity: 0

0 commit comments

Comments
 (0)