@platformos/platformos-check-common 0.0.10 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CLAUDE.md +150 -0
- package/dist/AugmentedPlatformOSDocset.js +1 -0
- package/dist/AugmentedPlatformOSDocset.js.map +1 -1
- package/dist/checks/deprecated-filter/index.js +15 -0
- package/dist/checks/deprecated-filter/index.js.map +1 -1
- package/dist/checks/graphql/index.d.ts +1 -0
- package/dist/checks/graphql/index.js +20 -7
- package/dist/checks/graphql/index.js.map +1 -1
- package/dist/checks/invalid-hash-assign-target/index.js +4 -3
- package/dist/checks/invalid-hash-assign-target/index.js.map +1 -1
- package/dist/checks/undefined-object/index.js +14 -13
- package/dist/checks/undefined-object/index.js.map +1 -1
- package/dist/checks/unknown-property/index.js +75 -10
- package/dist/checks/unknown-property/index.js.map +1 -1
- package/dist/checks/unknown-property/property-shape.js +14 -1
- package/dist/checks/unknown-property/property-shape.js.map +1 -1
- package/dist/checks/unused-assign/index.js +23 -1
- package/dist/checks/unused-assign/index.js.map +1 -1
- package/dist/checks/variable-name/index.js +4 -0
- package/dist/checks/variable-name/index.js.map +1 -1
- package/dist/frontmatter/index.d.ts +59 -0
- package/dist/frontmatter/index.js +301 -0
- package/dist/frontmatter/index.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/liquid-doc/arguments.js +5 -0
- package/dist/liquid-doc/arguments.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.js +3 -1
- package/dist/path.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/AugmentedPlatformOSDocset.ts +1 -0
- package/src/checks/deprecated-filter/index.spec.ts +41 -1
- package/src/checks/deprecated-filter/index.ts +17 -0
- package/src/checks/graphql/index.spec.ts +173 -0
- package/src/checks/graphql/index.ts +21 -10
- package/src/checks/invalid-hash-assign-target/index.spec.ts +26 -0
- package/src/checks/invalid-hash-assign-target/index.ts +6 -4
- package/src/checks/undefined-object/index.spec.ts +123 -19
- package/src/checks/undefined-object/index.ts +16 -18
- package/src/checks/unknown-property/index.spec.ts +133 -0
- package/src/checks/unknown-property/index.ts +84 -10
- package/src/checks/unknown-property/property-shape.ts +15 -1
- package/src/checks/unused-assign/index.spec.ts +74 -0
- package/src/checks/unused-assign/index.ts +26 -1
- package/src/checks/variable-name/index.spec.ts +9 -0
- package/src/checks/variable-name/index.ts +5 -0
- package/src/frontmatter/index.ts +344 -0
- package/src/index.ts +3 -0
- package/src/liquid-doc/arguments.ts +3 -0
- package/src/path.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@platformos/platformos-check-common",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"type-check": "tsc --noEmit"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@platformos/liquid-html-parser": "0.0.
|
|
29
|
+
"@platformos/liquid-html-parser": "0.0.11",
|
|
30
30
|
"graphql": "^16.12.0",
|
|
31
31
|
"js-yaml": "^4.1.1",
|
|
32
32
|
"jsonc-parser": "^3.3.1",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, describe, it } from 'vitest';
|
|
2
|
-
import { highlightedOffenses, runLiquidCheck } from '../../test';
|
|
2
|
+
import { applySuggestions, highlightedOffenses, runLiquidCheck } from '../../test';
|
|
3
3
|
import { DeprecatedFilter } from './index';
|
|
4
4
|
|
|
5
5
|
const mockDependencies = {
|
|
@@ -99,4 +99,44 @@ describe('Module: DeprecatedFilter', () => {
|
|
|
99
99
|
);
|
|
100
100
|
expect(offenses).toHaveLength(2);
|
|
101
101
|
});
|
|
102
|
+
|
|
103
|
+
it('should provide a suggestion to replace deprecated filter with recommended alternative', async () => {
|
|
104
|
+
const sourceCode = `{{ value | old_filter }}`;
|
|
105
|
+
|
|
106
|
+
const offenses = await runLiquidCheck(
|
|
107
|
+
DeprecatedFilter,
|
|
108
|
+
sourceCode,
|
|
109
|
+
'file.liquid',
|
|
110
|
+
mockDependencies,
|
|
111
|
+
);
|
|
112
|
+
expect(offenses).toHaveLength(1);
|
|
113
|
+
expect(offenses[0].suggest).toHaveLength(1);
|
|
114
|
+
expect(offenses[0].suggest![0].message).toEqual("Replace 'old_filter' with 'new_filter'");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should apply the suggestion to replace the filter name', async () => {
|
|
118
|
+
const sourceCode = `{{ value | old_filter }}`;
|
|
119
|
+
|
|
120
|
+
const offenses = await runLiquidCheck(
|
|
121
|
+
DeprecatedFilter,
|
|
122
|
+
sourceCode,
|
|
123
|
+
'file.liquid',
|
|
124
|
+
mockDependencies,
|
|
125
|
+
);
|
|
126
|
+
const suggestions = applySuggestions(sourceCode, offenses[0]);
|
|
127
|
+
expect(suggestions).toContain('{{ value | new_filter }}');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should not provide a suggestion when no replacement exists', async () => {
|
|
131
|
+
const sourceCode = `{{ value | deprecated_no_replacement }}`;
|
|
132
|
+
|
|
133
|
+
const offenses = await runLiquidCheck(
|
|
134
|
+
DeprecatedFilter,
|
|
135
|
+
sourceCode,
|
|
136
|
+
'file.liquid',
|
|
137
|
+
mockDependencies,
|
|
138
|
+
);
|
|
139
|
+
expect(offenses).toHaveLength(1);
|
|
140
|
+
expect(offenses[0].suggest).toBeUndefined();
|
|
141
|
+
});
|
|
102
142
|
});
|
|
@@ -39,10 +39,27 @@ export const DeprecatedFilter: LiquidCheckDefinition = {
|
|
|
39
39
|
|
|
40
40
|
const message = deprecatedFilterMessage(deprecatedFilter, recommendedFilter);
|
|
41
41
|
|
|
42
|
+
const filterText = node.source.slice(node.position.start, node.position.end);
|
|
43
|
+
const afterPipeIdx = filterText.indexOf('|') + 1;
|
|
44
|
+
const nameIdx =
|
|
45
|
+
afterPipeIdx + filterText.slice(afterPipeIdx).indexOf(deprecatedFilter.name);
|
|
46
|
+
const filterNameStart = node.position.start + nameIdx;
|
|
47
|
+
const filterNameEnd = filterNameStart + deprecatedFilter.name.length;
|
|
48
|
+
|
|
42
49
|
context.report({
|
|
43
50
|
message,
|
|
44
51
|
startIndex: node.position.start + 1,
|
|
45
52
|
endIndex: node.position.end,
|
|
53
|
+
suggest: recommendedFilter
|
|
54
|
+
? [
|
|
55
|
+
{
|
|
56
|
+
message: `Replace '${deprecatedFilter.name}' with '${recommendedFilter.name}'`,
|
|
57
|
+
fix: (corrector) => {
|
|
58
|
+
corrector.replace(filterNameStart, filterNameEnd, recommendedFilter.name);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
]
|
|
62
|
+
: undefined,
|
|
46
63
|
});
|
|
47
64
|
},
|
|
48
65
|
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { expect, describe, it } from 'vitest';
|
|
2
|
+
import { GraphQLCheck, lineToRange } from './index';
|
|
3
|
+
import { check } from '../../test';
|
|
4
|
+
|
|
5
|
+
const SCHEMA = `
|
|
6
|
+
type Query {
|
|
7
|
+
hello: String
|
|
8
|
+
users: [User]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type User {
|
|
12
|
+
id: ID
|
|
13
|
+
name: String
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const mockDependencies = {
|
|
18
|
+
platformosDocset: {
|
|
19
|
+
async graphQL() {
|
|
20
|
+
return SCHEMA;
|
|
21
|
+
},
|
|
22
|
+
async filters() {
|
|
23
|
+
return [];
|
|
24
|
+
},
|
|
25
|
+
async objects() {
|
|
26
|
+
return [];
|
|
27
|
+
},
|
|
28
|
+
async liquidDrops() {
|
|
29
|
+
return [];
|
|
30
|
+
},
|
|
31
|
+
async tags() {
|
|
32
|
+
return [];
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const noDeps = {
|
|
38
|
+
platformosDocset: {
|
|
39
|
+
async graphQL() {
|
|
40
|
+
return null;
|
|
41
|
+
},
|
|
42
|
+
async filters() {
|
|
43
|
+
return [];
|
|
44
|
+
},
|
|
45
|
+
async objects() {
|
|
46
|
+
return [];
|
|
47
|
+
},
|
|
48
|
+
async liquidDrops() {
|
|
49
|
+
return [];
|
|
50
|
+
},
|
|
51
|
+
async tags() {
|
|
52
|
+
return [];
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe('Module: GraphQLCheck', () => {
|
|
58
|
+
it('reports no offenses for a valid query', async () => {
|
|
59
|
+
const files = {
|
|
60
|
+
'app/graphql/my_query.graphql': '{ hello }',
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const offenses = await check(files, [GraphQLCheck], mockDependencies);
|
|
64
|
+
expect(offenses).to.be.empty;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('reports an offense for an unknown field', async () => {
|
|
68
|
+
const files = {
|
|
69
|
+
'app/graphql/my_query.graphql': '{ unknownField }',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const offenses = await check(files, [GraphQLCheck], mockDependencies);
|
|
73
|
+
expect(offenses).to.have.length(1);
|
|
74
|
+
expect(offenses[0].message).to.equal('Cannot query field "unknownField" on type "Query".');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('offense for unknown field spans only the affected line, not the entire file', async () => {
|
|
78
|
+
const query = `{
|
|
79
|
+
unknownField
|
|
80
|
+
}`;
|
|
81
|
+
const files = {
|
|
82
|
+
'app/graphql/my_query.graphql': query,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const offenses = await check(files, [GraphQLCheck], mockDependencies);
|
|
86
|
+
expect(offenses).to.have.length(1);
|
|
87
|
+
|
|
88
|
+
// Should point to line 2 (1-based), which is " unknownField"
|
|
89
|
+
// and NOT span to the end of the file
|
|
90
|
+
expect(offenses[0].start.line).to.equal(1); // 0-based: line index 1 = " unknownField"
|
|
91
|
+
expect(offenses[0].end.line).to.equal(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('reports a syntax error offense instead of swallowing it', async () => {
|
|
95
|
+
const files = {
|
|
96
|
+
'app/graphql/my_query.graphql': '{ unclosed {',
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const offenses = await check(files, [GraphQLCheck], mockDependencies);
|
|
100
|
+
expect(offenses).to.have.length(1);
|
|
101
|
+
expect(offenses[0].message).to.include('Syntax Error');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('syntax error offense points to the actual error line, not the whole file', async () => {
|
|
105
|
+
// unclosed brace on line 3 causes a parse error — graphql-js will report the exact location
|
|
106
|
+
const query = `{
|
|
107
|
+
hello
|
|
108
|
+
unclosed {
|
|
109
|
+
`;
|
|
110
|
+
const files = {
|
|
111
|
+
'app/graphql/my_query.graphql': query,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const offenses = await check(files, [GraphQLCheck], mockDependencies);
|
|
115
|
+
expect(offenses).to.have.length(1);
|
|
116
|
+
expect(offenses[0].message).to.include('Syntax Error');
|
|
117
|
+
// Offense spans exactly one line (the error line), NOT the whole file
|
|
118
|
+
expect(offenses[0].start.line).to.equal(offenses[0].end.line);
|
|
119
|
+
// And that line is not the last line of the file (i.e. not spanning to the end)
|
|
120
|
+
expect(offenses[0].end.line).to.be.lessThan(3); // file has 4 lines (0-indexed: 0-3)
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('reports no offenses when platformosDocset.graphQL returns null', async () => {
|
|
124
|
+
const files = {
|
|
125
|
+
'app/graphql/my_query.graphql': '{ unknownField }',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const offenses = await check(files, [GraphQLCheck], noDeps);
|
|
129
|
+
expect(offenses).to.be.empty;
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('Unit: lineToRange', () => {
|
|
134
|
+
const TEXT = 'line1\nline2\nline3';
|
|
135
|
+
|
|
136
|
+
it('returns correct range for line 1', () => {
|
|
137
|
+
expect(lineToRange(TEXT, 1)).to.eql([0, 5]); // "line1"
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('returns correct range for line 2', () => {
|
|
141
|
+
expect(lineToRange(TEXT, 2)).to.eql([6, 11]); // "line2"
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('returns correct range for line 3', () => {
|
|
145
|
+
expect(lineToRange(TEXT, 3)).to.eql([12, 17]); // "line3"
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('clamps line 0 to first line instead of spanning the whole file', () => {
|
|
149
|
+
const [start, end] = lineToRange(TEXT, 0);
|
|
150
|
+
expect(start).to.equal(0);
|
|
151
|
+
expect(end).to.equal(5); // "line1" length = 5, not TEXT.length (17)
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('clamps line beyond last to last line instead of spanning the whole file', () => {
|
|
155
|
+
const [start, end] = lineToRange(TEXT, 999);
|
|
156
|
+
expect(start).to.equal(12);
|
|
157
|
+
expect(end).to.equal(17); // "line3"
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('handles single-line text with line 0', () => {
|
|
161
|
+
const [start, end] = lineToRange('hello', 0);
|
|
162
|
+
expect(start).to.equal(0);
|
|
163
|
+
expect(end).to.equal(5); // entire single line, NOT text.length (which happens to be the same here)
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('does not return the whole file when line is 0', () => {
|
|
167
|
+
const longText = 'first line\nsecond line\nthird line';
|
|
168
|
+
const [, end] = lineToRange(longText, 0);
|
|
169
|
+
// Should be end of first line (10), not end of whole text (33)
|
|
170
|
+
expect(end).to.equal(10);
|
|
171
|
+
expect(end).to.not.equal(longText.length);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
import { GraphQLCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
2
2
|
import { parse } from 'graphql/language';
|
|
3
|
-
import { buildSchema, validate } from 'graphql';
|
|
3
|
+
import { buildSchema, GraphQLError, validate } from 'graphql';
|
|
4
4
|
|
|
5
|
-
function lineToRange(text: string, line: number): [number, number] {
|
|
5
|
+
export function lineToRange(text: string, line: number): [number, number] {
|
|
6
6
|
const lines = text.split(/\r?\n/);
|
|
7
|
-
|
|
8
|
-
if (line < 1 || line > lines.length) {
|
|
9
|
-
return [0, text.length];
|
|
10
|
-
}
|
|
7
|
+
const clampedLine = Math.max(1, Math.min(line, lines.length));
|
|
11
8
|
|
|
12
9
|
let start = 0;
|
|
13
|
-
for (let i = 0; i <
|
|
10
|
+
for (let i = 0; i < clampedLine - 1; i++) {
|
|
14
11
|
start += lines[i].length + 1;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const end = start + lines[
|
|
14
|
+
const end = start + lines[clampedLine - 1].length;
|
|
18
15
|
return [start, end];
|
|
19
16
|
}
|
|
20
17
|
|
|
@@ -42,11 +39,25 @@ export const GraphQLCheck: GraphQLCheckDefinition = {
|
|
|
42
39
|
|
|
43
40
|
const graphQLSchema = buildSchema(graphQLSchemaString);
|
|
44
41
|
|
|
45
|
-
|
|
42
|
+
let document;
|
|
43
|
+
try {
|
|
44
|
+
document = parse(content);
|
|
45
|
+
} catch (e) {
|
|
46
|
+
if (e instanceof GraphQLError) {
|
|
47
|
+
const [start, end] = lineToRange(content, e.locations?.[0]?.line ?? 1);
|
|
48
|
+
context.report({
|
|
49
|
+
message: e.message,
|
|
50
|
+
startIndex: start,
|
|
51
|
+
endIndex: end,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
46
57
|
const errors = validate(graphQLSchema, document);
|
|
47
58
|
|
|
48
59
|
errors.forEach((error) => {
|
|
49
|
-
const [start, end] = lineToRange(content, error.locations?.[0]
|
|
60
|
+
const [start, end] = lineToRange(content, error.locations?.[0]?.line ?? 0);
|
|
50
61
|
context.report({
|
|
51
62
|
message: error.message,
|
|
52
63
|
startIndex: start,
|
|
@@ -119,6 +119,32 @@ describe('Module: InvalidHashAssignTarget', () => {
|
|
|
119
119
|
expect(offenses).toHaveLength(0);
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
+
it('should not report an error when hash_assign is used on a function return with variable partial', async () => {
|
|
123
|
+
const app: MockApp = {
|
|
124
|
+
'file.liquid': `
|
|
125
|
+
{% assign partial_name = 'lib/get_data' %}
|
|
126
|
+
{% function data = partial_name %}
|
|
127
|
+
{% hash_assign data['extra'] = 'value' %}
|
|
128
|
+
`,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const offenses = await check(app, [InvalidHashAssignTarget]);
|
|
132
|
+
expect(offenses).toHaveLength(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should not report an error when function uses hash-access result target and hash_assign follows', async () => {
|
|
136
|
+
const app: MockApp = {
|
|
137
|
+
'file.liquid': `
|
|
138
|
+
{% parse_json my_hash %}{}{% endparse_json %}
|
|
139
|
+
{% function my_hash['result'] = 'lib/get_data' %}
|
|
140
|
+
{% hash_assign my_hash['extra'] = 'value' %}
|
|
141
|
+
`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const offenses = await check(app, [InvalidHashAssignTarget]);
|
|
145
|
+
expect(offenses).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
122
148
|
it('should track reassignment and report error on new type', async () => {
|
|
123
149
|
const app: MockApp = {
|
|
124
150
|
'file.liquid': `
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
HashAssignMarkup,
|
|
8
8
|
GraphQLMarkup,
|
|
9
9
|
GraphQLInlineMarkup,
|
|
10
|
+
FunctionMarkup,
|
|
10
11
|
} from '@platformos/liquid-html-parser';
|
|
11
12
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
12
13
|
import { isError } from '../../utils';
|
|
@@ -229,12 +230,13 @@ export const InvalidHashAssignTarget: LiquidCheckDefinition = {
|
|
|
229
230
|
|
|
230
231
|
// {% function result = 'path' %}
|
|
231
232
|
if (node.name === NamedTags.function && typeof node.markup !== 'string') {
|
|
232
|
-
const markup = node.markup as
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
const markup = node.markup as FunctionMarkup;
|
|
234
|
+
const varName = markup.name.name;
|
|
235
|
+
if (varName) {
|
|
236
|
+
closeTypeRange(varName, node.position.start);
|
|
235
237
|
// Function returns are untyped unless we can infer them
|
|
236
238
|
variableTypes.push({
|
|
237
|
-
name:
|
|
239
|
+
name: varName,
|
|
238
240
|
type: 'untyped',
|
|
239
241
|
range: [node.position.end],
|
|
240
242
|
});
|
|
@@ -145,6 +145,116 @@ describe('Module: UndefinedObject', () => {
|
|
|
145
145
|
expect(offenses).toHaveLength(0);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
it('should report an offense when function result variable is used before its definition', async () => {
|
|
149
|
+
const sourceCode = `
|
|
150
|
+
{{ a }}
|
|
151
|
+
{% function a = 'test' %}
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
155
|
+
|
|
156
|
+
expect(offenses).toHaveLength(1);
|
|
157
|
+
expect(offenses[0].message).toBe("Unknown object 'a' used.");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not report an offense for multiple function result variables', async () => {
|
|
161
|
+
const sourceCode = `
|
|
162
|
+
{% function result1 = 'partial_one' %}
|
|
163
|
+
{% function result2 = 'partial_two' %}
|
|
164
|
+
{{ result1 }}
|
|
165
|
+
{{ result2 }}
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
169
|
+
|
|
170
|
+
expect(offenses).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should not register a scope variable when function target is a hash/array access', async () => {
|
|
174
|
+
const sourceCode = `
|
|
175
|
+
{% parse_json my_hash %}{"key": "value"}{% endparse_json %}
|
|
176
|
+
{% function my_hash['result'] = 'test' %}
|
|
177
|
+
{{ my_hash }}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
181
|
+
|
|
182
|
+
// my_hash is defined via parse_json; function hash-access target does not shadow it
|
|
183
|
+
expect(offenses).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should report an offense when a variable partial in include is undefined', async () => {
|
|
187
|
+
const sourceCode = `
|
|
188
|
+
{% include undefined_partial %}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
192
|
+
|
|
193
|
+
expect(offenses).toHaveLength(1);
|
|
194
|
+
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should not report an offense when a variable partial in include is defined', async () => {
|
|
198
|
+
const sourceCode = `
|
|
199
|
+
{% assign partial_name = 'some/partial' %}
|
|
200
|
+
{% include partial_name %}
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
204
|
+
|
|
205
|
+
expect(offenses).toHaveLength(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should report an offense when a variable partial in function is undefined', async () => {
|
|
209
|
+
const sourceCode = `
|
|
210
|
+
{% function result = undefined_partial %}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
214
|
+
|
|
215
|
+
expect(offenses).toHaveLength(1);
|
|
216
|
+
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should not report an offense for the result variable itself in function tag', async () => {
|
|
220
|
+
const sourceCode = `
|
|
221
|
+
{% function result = undefined_partial %}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
225
|
+
|
|
226
|
+
// only 'undefined_partial' should be reported, not 'result'
|
|
227
|
+
expect(offenses.every((o) => o.message !== "Unknown object 'result' used.")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should report offenses for lookup key variables in function result target and partial', async () => {
|
|
231
|
+
const sourceCode = `
|
|
232
|
+
{% parse_json my_hash %}{}{% endparse_json %}
|
|
233
|
+
{% function my_hash[lookup_key] = my_hash[path_var] %}
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
237
|
+
|
|
238
|
+
const messages = offenses.map((o) => o.message);
|
|
239
|
+
// lookup_key and path_var are undefined; my_hash is defined
|
|
240
|
+
expect(messages).toContain("Unknown object 'lookup_key' used.");
|
|
241
|
+
expect(messages).toContain("Unknown object 'path_var' used.");
|
|
242
|
+
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should check the partial variable in function but not the hash-access result target base', async () => {
|
|
246
|
+
const sourceCode = `
|
|
247
|
+
{% parse_json my_hash %}{}{% endparse_json %}
|
|
248
|
+
{% function my_hash['key'] = undefined_partial %}
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
252
|
+
|
|
253
|
+
const messages = offenses.map((o) => o.message);
|
|
254
|
+
expect(messages).toContain("Unknown object 'undefined_partial' used.");
|
|
255
|
+
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
256
|
+
});
|
|
257
|
+
|
|
148
258
|
it('should report an offense when object is defined in a for loop but used outside of the scope (in scenarios where the same variable has multiple scopes in the file)', async () => {
|
|
149
259
|
const sourceCode = `
|
|
150
260
|
{% for c in collections %}
|
|
@@ -359,10 +469,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
359
469
|
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
|
|
360
470
|
});
|
|
361
471
|
|
|
362
|
-
it('should report an offense when
|
|
472
|
+
it('should report an offense when undefined variable is used inside background block', async () => {
|
|
363
473
|
const sourceCode = `
|
|
364
|
-
{% background
|
|
365
|
-
{{
|
|
474
|
+
{% background source_type: 'some form' %}
|
|
475
|
+
{{ undefined_var }}
|
|
366
476
|
{% endbackground %}
|
|
367
477
|
`;
|
|
368
478
|
|
|
@@ -371,12 +481,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
371
481
|
expect(offenses).toHaveLength(1);
|
|
372
482
|
});
|
|
373
483
|
|
|
374
|
-
it('should not report an offense when job_id is used after background
|
|
484
|
+
it('should not report an offense when job_id is used after background file-based tag', async () => {
|
|
375
485
|
const sourceCode = `
|
|
376
|
-
{% background
|
|
377
|
-
|
|
378
|
-
{% endbackground %}
|
|
379
|
-
{{ job_id }}
|
|
486
|
+
{% background my_job = 'some_partial' %}
|
|
487
|
+
{{ my_job }}
|
|
380
488
|
`;
|
|
381
489
|
|
|
382
490
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
@@ -384,12 +492,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
384
492
|
expect(offenses).toHaveLength(0);
|
|
385
493
|
});
|
|
386
494
|
|
|
387
|
-
it('should not report an offense when job_id is used after background
|
|
495
|
+
it('should not report an offense when job_id is used after background file-based tag with named args', async () => {
|
|
388
496
|
const sourceCode = `
|
|
389
|
-
{% background
|
|
390
|
-
|
|
391
|
-
{% endbackground %}
|
|
392
|
-
{{ job_id }}
|
|
497
|
+
{% background my_job = 'some_partial', source_type: 'some form' %}
|
|
498
|
+
{{ my_job }}
|
|
393
499
|
`;
|
|
394
500
|
|
|
395
501
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
@@ -397,18 +503,16 @@ describe('Module: UndefinedObject', () => {
|
|
|
397
503
|
expect(offenses).toHaveLength(0);
|
|
398
504
|
});
|
|
399
505
|
|
|
400
|
-
it('should report an offense when job_id is used before background
|
|
506
|
+
it('should report an offense when job_id is used before background file-based tag', async () => {
|
|
401
507
|
const sourceCode = `
|
|
402
|
-
{{
|
|
403
|
-
{% background
|
|
404
|
-
{% assign a = 5 %}
|
|
405
|
-
{% endbackground %}
|
|
508
|
+
{{ my_job }}
|
|
509
|
+
{% background my_job = 'some_partial' %}
|
|
406
510
|
`;
|
|
407
511
|
|
|
408
512
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
409
513
|
|
|
410
514
|
expect(offenses).toHaveLength(1);
|
|
411
|
-
expect(offenses.map((e) => e.message)).toEqual(["Unknown object '
|
|
515
|
+
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_job' used."]);
|
|
412
516
|
});
|
|
413
517
|
|
|
414
518
|
it('should not report an offense when object is defined with a parse_json tag', async () => {
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
LiquidTagGraphQL,
|
|
19
19
|
LiquidTagParseJson,
|
|
20
20
|
LiquidTagBackground,
|
|
21
|
-
|
|
21
|
+
BackgroundMarkup,
|
|
22
22
|
} from '@platformos/liquid-html-parser';
|
|
23
23
|
import { LiquidCheckDefinition, Severity, SourceCodeType, PlatformOSDocset } from '../../types';
|
|
24
24
|
import { isError, last } from '../../utils';
|
|
@@ -108,9 +108,13 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (node.name === 'function') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const fnName = (node.markup as FunctionMarkup).name;
|
|
112
|
+
// Only register simple variable names (not hash/array mutations like hash['key'])
|
|
113
|
+
if (fnName.lookups.length === 0 && fnName.name !== null) {
|
|
114
|
+
indexVariableScope(fnName.name, {
|
|
115
|
+
start: node.position.end,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
if (node.name === 'layout') {
|
|
@@ -148,8 +152,8 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (isLiquidTagBackground(node)) {
|
|
151
|
-
indexVariableScope(node.markup.jobId
|
|
152
|
-
start: node.
|
|
155
|
+
indexVariableScope(node.markup.jobId, {
|
|
156
|
+
start: node.position.end,
|
|
153
157
|
});
|
|
154
158
|
}
|
|
155
159
|
},
|
|
@@ -160,9 +164,8 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
160
164
|
const parent = last(ancestors);
|
|
161
165
|
if (isLiquidTag(parent) && isLiquidTagCapture(parent)) return;
|
|
162
166
|
if (isLiquidTag(parent) && isLiquidTagParseJson(parent)) return;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (isBackgroundInlineMarkup(parent) && parent.jobId === node) return;
|
|
167
|
+
// Skip the result variable of function tags (it's a definition, not a usage)
|
|
168
|
+
if (isFunctionMarkup(parent) && parent.name === node) return;
|
|
166
169
|
|
|
167
170
|
variables.push(node);
|
|
168
171
|
},
|
|
@@ -299,19 +302,14 @@ function isLiquidTagDecrement(node: LiquidTag): node is LiquidTagDecrement {
|
|
|
299
302
|
|
|
300
303
|
function isLiquidTagBackground(
|
|
301
304
|
node: LiquidTag,
|
|
302
|
-
): node is LiquidTagBackground & { markup:
|
|
305
|
+
): node is LiquidTagBackground & { markup: BackgroundMarkup } {
|
|
303
306
|
return (
|
|
304
307
|
node.name === NamedTags.background &&
|
|
305
308
|
typeof node.markup !== 'string' &&
|
|
306
|
-
node.markup.type === NodeTypes.
|
|
309
|
+
node.markup.type === NodeTypes.BackgroundMarkup
|
|
307
310
|
);
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
function
|
|
311
|
-
return
|
|
312
|
-
typeof node === 'object' &&
|
|
313
|
-
node !== null &&
|
|
314
|
-
'type' in node &&
|
|
315
|
-
node.type === NodeTypes.BackgroundInlineMarkup
|
|
316
|
-
);
|
|
313
|
+
function isFunctionMarkup(node?: LiquidHtmlNode): node is FunctionMarkup {
|
|
314
|
+
return node?.type === NodeTypes.FunctionMarkup;
|
|
317
315
|
}
|