td-ai-tools 1.2.4 → 1.2.6
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/package.json +1 -1
- package/skills/README.md +1 -1
- package/skills/shopify-lint/SKILL.md +4 -4
- package/skills/shopify-lint/agents/openai.yaml +1 -1
- package/skills/shopify-lint/hooks/codex-stop.sh +1 -1
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +17 -1
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +16 -1
- package/skills/shopify-lint/theme-check-theory/README.md +89 -18
- package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +16 -1
- package/skills/shopify-lint/theme-check-theory/package-lock.json +2 -2
- package/skills/shopify-lint/theme-check-theory/package.json +1 -1
- package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.test.ts +54 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.ts +77 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +102 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +171 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.test.ts +76 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.ts +49 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.test.ts +77 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.ts +42 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.test.ts +301 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.ts +129 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +1 -1
- package/skills/shopify-lint/theme-check-theory/src/index.test.ts +17 -4
- package/skills/shopify-lint/theme-check-theory/src/index.ts +20 -6
- package/skills/shopify-lint/theme-check-theory/src/utils/null-guard.ts +79 -0
- package/skills/shopify-lint/theme-check-theory/src/utils/nullable-output-check.ts +90 -0
- package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +0 -198
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +0 -143
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
check,
|
|
3
|
-
runLiquidCheck,
|
|
4
|
-
} from '@shopify/theme-check-common/dist/test/test-helper';
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
-
|
|
7
|
-
import { liquidSection } from '../test-utils';
|
|
8
|
-
import {
|
|
9
|
-
DEFAULT_TEXTUAL_SETTING_TYPES,
|
|
10
|
-
UnguardedTextSetting,
|
|
11
|
-
} from './unguarded-text-setting';
|
|
12
|
-
|
|
13
|
-
describe('UnguardedTextSetting', () => {
|
|
14
|
-
it('reports raw outputs of all default text-like section and block setting types', async () => {
|
|
15
|
-
const settings = DEFAULT_TEXTUAL_SETTING_TYPES.map((type) => ({
|
|
16
|
-
type,
|
|
17
|
-
id: `section_${type}`,
|
|
18
|
-
}));
|
|
19
|
-
const blockSettings = DEFAULT_TEXTUAL_SETTING_TYPES.map((type) => ({
|
|
20
|
-
type,
|
|
21
|
-
id: `block_${type}`,
|
|
22
|
-
}));
|
|
23
|
-
const outputs = [
|
|
24
|
-
...settings.map(({ id }) => `{{ section.settings.${id} }}`),
|
|
25
|
-
...blockSettings.map(({ id }) => `{{ block.settings.${id} }}`),
|
|
26
|
-
].join('\n');
|
|
27
|
-
const source = liquidSection(outputs, settings, [
|
|
28
|
-
{ type: 'card', name: 'Card', settings: blockSettings },
|
|
29
|
-
]);
|
|
30
|
-
|
|
31
|
-
const offenses = await runLiquidCheck(UnguardedTextSetting, source);
|
|
32
|
-
|
|
33
|
-
expect(offenses).toHaveLength(DEFAULT_TEXTUAL_SETTING_TYPES.length * 2);
|
|
34
|
-
expect(offenses[0].message).toBe(
|
|
35
|
-
"The text-like setting 'section_text' is output without a presence guard or default",
|
|
36
|
-
);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('ignores non-textual settings by default', async () => {
|
|
40
|
-
const source = liquidSection(
|
|
41
|
-
`
|
|
42
|
-
{{ section.settings.enabled }}
|
|
43
|
-
{{ section.settings.spacing }}
|
|
44
|
-
{{ section.settings.image }}
|
|
45
|
-
`,
|
|
46
|
-
[
|
|
47
|
-
{ type: 'checkbox', id: 'enabled' },
|
|
48
|
-
{ type: 'range', id: 'spacing' },
|
|
49
|
-
{ type: 'image_picker', id: 'image' },
|
|
50
|
-
],
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it.each([
|
|
57
|
-
[
|
|
58
|
-
'if',
|
|
59
|
-
'{% if section.settings.heading %}{{ section.settings.heading }}{% endif %}',
|
|
60
|
-
],
|
|
61
|
-
[
|
|
62
|
-
'unless',
|
|
63
|
-
'{% unless section.settings.heading == blank %}{{ section.settings.heading }}{% endunless %}',
|
|
64
|
-
],
|
|
65
|
-
[
|
|
66
|
-
'elsif',
|
|
67
|
-
'{% if product %}x{% elsif section.settings.heading %}{{ section.settings.heading }}{% endif %}',
|
|
68
|
-
],
|
|
69
|
-
[
|
|
70
|
-
'case',
|
|
71
|
-
'{% case section.settings.heading %}{% when "Hello" %}{{ section.settings.heading }}{% endcase %}',
|
|
72
|
-
],
|
|
73
|
-
])('accepts a setting tested in an %s conditional', async (_, liquid) => {
|
|
74
|
-
const source = liquidSection(liquid, [{ type: 'text', id: 'heading' }]);
|
|
75
|
-
|
|
76
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('accepts logical and comparison presence tests', async () => {
|
|
80
|
-
const source = liquidSection(
|
|
81
|
-
`
|
|
82
|
-
{% if section.settings.heading != blank and section.settings.subtitle %}
|
|
83
|
-
{{ section.settings.heading }} {{ section.settings.subtitle }}
|
|
84
|
-
{% endif %}
|
|
85
|
-
`,
|
|
86
|
-
[
|
|
87
|
-
{ type: 'text', id: 'heading' },
|
|
88
|
-
{ type: 'textarea', id: 'subtitle' },
|
|
89
|
-
],
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('does not mistake an unrelated enclosing condition for a setting presence guard', async () => {
|
|
96
|
-
const source = liquidSection(
|
|
97
|
-
'{% if product.available %}{{ section.settings.heading }}{% endif %}',
|
|
98
|
-
[{ type: 'text', id: 'heading' }],
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
const offenses = await runLiquidCheck(UnguardedTextSetting, source);
|
|
102
|
-
|
|
103
|
-
expect(offenses).toHaveLength(1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('accepts output with a default filter', async () => {
|
|
107
|
-
const source = liquidSection(
|
|
108
|
-
"{{ section.settings.heading | strip | default: 'Welcome' }}",
|
|
109
|
-
[{ type: 'text', id: 'heading' }],
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('does not treat use as another expression default as handling the setting', async () => {
|
|
116
|
-
const source = liquidSection(
|
|
117
|
-
"{{ product.title | default: section.settings.heading }} {{ section.settings.heading }}",
|
|
118
|
-
[{ type: 'text', id: 'heading' }],
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
const offenses = await runLiquidCheck(UnguardedTextSetting, source);
|
|
122
|
-
|
|
123
|
-
expect(offenses).toHaveLength(1);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('applies handling coarsely to every output of that setting in the file', async () => {
|
|
127
|
-
const source = liquidSection(
|
|
128
|
-
`
|
|
129
|
-
{{ section.settings.heading }}
|
|
130
|
-
{% if section.settings.heading %}{{ section.settings.heading }}{% endif %}
|
|
131
|
-
{{ section.settings.heading }}
|
|
132
|
-
`,
|
|
133
|
-
[{ type: 'text', id: 'heading' }],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('reports every raw output when the setting is never handled', async () => {
|
|
140
|
-
const source = liquidSection(
|
|
141
|
-
'{{ section.settings.heading }} / {{ section.settings.heading | escape }}',
|
|
142
|
-
[{ type: 'text', id: 'heading' }],
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
const offenses = await runLiquidCheck(UnguardedTextSetting, source);
|
|
146
|
-
|
|
147
|
-
expect(offenses).toHaveLength(2);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('only considers variable output tags, not assignments or echo tags', async () => {
|
|
151
|
-
const source = liquidSection(
|
|
152
|
-
'{% assign heading = section.settings.heading %}{% echo section.settings.heading %}',
|
|
153
|
-
[{ type: 'text', id: 'heading' }],
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('supports textualSettingTypes configuration', async () => {
|
|
160
|
-
const source = liquidSection('{{ section.settings.image }}', [
|
|
161
|
-
{ type: 'image_picker', id: 'image' },
|
|
162
|
-
]);
|
|
163
|
-
|
|
164
|
-
const offenses = await check(
|
|
165
|
-
{ 'sections/test.liquid': source },
|
|
166
|
-
[UnguardedTextSetting],
|
|
167
|
-
{},
|
|
168
|
-
{
|
|
169
|
-
UnguardedTextSetting: {
|
|
170
|
-
enabled: true,
|
|
171
|
-
textualSettingTypes: ['image_picker'],
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(offenses).toHaveLength(1);
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('ignores dynamic output while still checking statically known outputs', async () => {
|
|
180
|
-
const source = liquidSection(
|
|
181
|
-
'{{ section.settings[key] }} {{ section.settings.heading }}',
|
|
182
|
-
[{ type: 'text', id: 'heading' }],
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
const offenses = await runLiquidCheck(UnguardedTextSetting, source);
|
|
186
|
-
|
|
187
|
-
expect(offenses).toHaveLength(1);
|
|
188
|
-
expect(offenses[0].message).toContain("'heading'");
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it.each([
|
|
192
|
-
['a file without schema', '{{ section.settings.heading }}'],
|
|
193
|
-
['an invalid schema', '{{ section.settings.heading }}{% schema %}{ nope {% endschema %}'],
|
|
194
|
-
])('stays silent for %s', async (_, source) => {
|
|
195
|
-
expect(await runLiquidCheck(UnguardedTextSetting, source)).toEqual([]);
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LiquidCheckDefinition,
|
|
3
|
-
SchemaProp,
|
|
4
|
-
Severity,
|
|
5
|
-
SourceCodeType,
|
|
6
|
-
} from '@shopify/theme-check-common';
|
|
7
|
-
import {
|
|
8
|
-
NodeTypes,
|
|
9
|
-
} from '@shopify/liquid-html-parser';
|
|
10
|
-
import type {
|
|
11
|
-
LiquidHtmlNode,
|
|
12
|
-
LiquidVariableOutput,
|
|
13
|
-
LiquidVariableLookup,
|
|
14
|
-
} from '@shopify/liquid-html-parser';
|
|
15
|
-
|
|
16
|
-
import {
|
|
17
|
-
getSettingDeclarations,
|
|
18
|
-
getSettingReference,
|
|
19
|
-
settingKey,
|
|
20
|
-
} from '../utils/ast';
|
|
21
|
-
import type { SettingDeclaration } from '../utils/ast';
|
|
22
|
-
|
|
23
|
-
export const DEFAULT_TEXTUAL_SETTING_TYPES = [
|
|
24
|
-
'text',
|
|
25
|
-
'textarea',
|
|
26
|
-
'richtext',
|
|
27
|
-
'inline_richtext',
|
|
28
|
-
'html',
|
|
29
|
-
'liquid',
|
|
30
|
-
'url',
|
|
31
|
-
] as const;
|
|
32
|
-
|
|
33
|
-
const schema = {
|
|
34
|
-
textualSettingTypes: SchemaProp.array(
|
|
35
|
-
SchemaProp.string(),
|
|
36
|
-
[...DEFAULT_TEXTUAL_SETTING_TYPES],
|
|
37
|
-
),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const UnguardedTextSetting: LiquidCheckDefinition<typeof schema> = {
|
|
41
|
-
meta: {
|
|
42
|
-
code: 'UnguardedTextSetting',
|
|
43
|
-
name: 'Require guards for text-like settings',
|
|
44
|
-
docs: {
|
|
45
|
-
description: 'Reports text-like settings output without a presence guard or default filter.',
|
|
46
|
-
recommended: true,
|
|
47
|
-
},
|
|
48
|
-
type: SourceCodeType.LiquidHtml,
|
|
49
|
-
severity: Severity.WARNING,
|
|
50
|
-
schema,
|
|
51
|
-
},
|
|
52
|
-
create(context) {
|
|
53
|
-
const declarations: SettingDeclaration[] = [];
|
|
54
|
-
const handledSettings = new Set<string>();
|
|
55
|
-
const outputs: Array<{
|
|
56
|
-
id: string;
|
|
57
|
-
key: string;
|
|
58
|
-
node: LiquidVariableOutput;
|
|
59
|
-
}> = [];
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
async LiquidRawTag(node) {
|
|
63
|
-
const schemaDeclarations = getSettingDeclarations(node);
|
|
64
|
-
if (schemaDeclarations) declarations.push(...schemaDeclarations);
|
|
65
|
-
},
|
|
66
|
-
|
|
67
|
-
async LiquidVariableOutput(node) {
|
|
68
|
-
if (typeof node.markup === 'string') return;
|
|
69
|
-
|
|
70
|
-
const expression = node.markup.expression;
|
|
71
|
-
if (expression.type !== NodeTypes.VariableLookup) return;
|
|
72
|
-
|
|
73
|
-
const reference = getSettingReference(expression);
|
|
74
|
-
if (!reference || reference.dynamic || !reference.id) return;
|
|
75
|
-
|
|
76
|
-
const key = settingKey({ id: reference.id, scope: reference.scope });
|
|
77
|
-
outputs.push({ id: reference.id, key, node });
|
|
78
|
-
|
|
79
|
-
if (node.markup.filters.some(({ name }) => name === 'default')) {
|
|
80
|
-
handledSettings.add(key);
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
async VariableLookup(node, ancestors) {
|
|
85
|
-
const reference = getSettingReference(node);
|
|
86
|
-
if (!reference || reference.dynamic || !reference.id) return;
|
|
87
|
-
if (!isConditionalTest(node, ancestors)) return;
|
|
88
|
-
|
|
89
|
-
handledSettings.add(settingKey({ id: reference.id, scope: reference.scope }));
|
|
90
|
-
},
|
|
91
|
-
|
|
92
|
-
async onCodePathEnd() {
|
|
93
|
-
const textualTypes = new Set(context.settings.textualSettingTypes);
|
|
94
|
-
const textualSettings = new Set(
|
|
95
|
-
declarations
|
|
96
|
-
.filter(({ type }) => textualTypes.has(type))
|
|
97
|
-
.map(settingKey),
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
for (const output of outputs) {
|
|
101
|
-
if (!textualSettings.has(output.key) || handledSettings.has(output.key)) continue;
|
|
102
|
-
|
|
103
|
-
context.report({
|
|
104
|
-
message:
|
|
105
|
-
`The text-like setting '${output.id}' is output without a presence guard or default`,
|
|
106
|
-
startIndex: output.node.position.start,
|
|
107
|
-
endIndex: output.node.position.end,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
function isConditionalTest(node: LiquidVariableLookup, ancestors: LiquidHtmlNode[]): boolean {
|
|
116
|
-
return ancestors.some((ancestor) => {
|
|
117
|
-
if (ancestor.type === NodeTypes.LiquidTag) {
|
|
118
|
-
if (!['if', 'unless', 'case'].includes(ancestor.name)) return false;
|
|
119
|
-
return containsNode(ancestor.markup, node);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (ancestor.type === NodeTypes.LiquidBranch && ancestor.name === 'elsif') {
|
|
123
|
-
return containsNode(ancestor.markup, node);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return false;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function containsNode(markup: unknown, node: LiquidVariableLookup): boolean {
|
|
131
|
-
if (!markup || typeof markup !== 'object' || !('position' in markup)) return false;
|
|
132
|
-
|
|
133
|
-
const position = (markup as { position?: { start: number; end: number } }).position;
|
|
134
|
-
return Boolean(
|
|
135
|
-
position &&
|
|
136
|
-
position.start <= node.position.start &&
|
|
137
|
-
position.end >= node.position.end,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// v1 intentionally records a guard for the entire file. A branch-precise
|
|
142
|
-
// version can use the visitor `ancestors` argument to bind handling to the live
|
|
143
|
-
// LiquidBranch containing each output.
|