Skip to content

Commit b4d49dc

Browse files
feat: Lint rule for unsupported js (#2299)
1 parent d7fb077 commit b4d49dc

6 files changed

Lines changed: 701 additions & 1 deletion

File tree

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export default defineConfig([
4646
| [no-invalid-assignment](docs/rules/no-invalid-assignment.md) | Disallow assignments that will generate invalid WGSL || |
4747
| [no-math](docs/rules/no-math.md) | Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions | ||
4848
| [no-uninitialized-variables](docs/rules/no-uninitialized-variables.md) | Disallow variable declarations without initializers inside 'use gpu' functions || |
49+
| [no-unsupported-syntax](docs/rules/no-unsupported-syntax.md) | Disallow JS syntax that will not be parsed into valid WGSL. || |
4950
| [no-unwrapped-objects](docs/rules/no-unwrapped-objects.md) | Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns) || |
5051

5152
<!-- end auto-generated rules list -->
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# typegpu/no-unsupported-syntax
2+
3+
📝 Disallow JS syntax that will not be parsed into valid WGSL.
4+
5+
🚨 This rule is enabled in the ⭐ `recommended` config.
6+
7+
<!-- end auto-generated rule header -->
8+
9+
## Rule details
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```ts
14+
const fn = () => {
15+
'use gpu';
16+
const helper = (n) => 2 * n; // ArrowFunctionExpression
17+
return helper(1);
18+
}
19+
```
20+
```ts
21+
const fn = () => {
22+
'use gpu';
23+
const myStruct = Struct({prop: 1});
24+
const otherStruct = Struct({...myStruct}); // SpreadElement
25+
return otherStruct;
26+
}
27+
```
28+
```ts
29+
const fn = () => {
30+
'use gpu';
31+
throw new Error(); // ThrowStatement
32+
}
33+
```
34+
35+
Examples of **correct** code for this rule:
36+
37+
```ts
38+
const fn = (a) => {
39+
'use gpu';
40+
let counter = 0;
41+
let i = a;
42+
while (i) {
43+
if (i % 2 === 1) {
44+
counter += 1;
45+
}
46+
i >>= 1;
47+
}
48+
return otherFn(counter);
49+
}
50+
```
51+
```ts
52+
const fn = () => {
53+
'use gpu';
54+
let a = 0;
55+
const arr = [1, 2, 3];
56+
for (const item of arr) {
57+
a += item;
58+
}
59+
return a;
60+
}
61+
```
62+
63+
> [!NOTE]
64+
> Note that it is possible that TypeGPU starts/stops supporting JS syntax from version to version.
65+
> Make sure that the minor versions of `typegpu` and `eslint-plugin-typegpu` match.

packages/eslint-plugin/src/configs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import { noUnwrappedObjects } from './rules/noUnwrappedObjects.ts';
44
import { noMath } from './rules/noMath.ts';
55
import { noUninitializedVariables } from './rules/noUninitializedVariables.ts';
66
import { noInvalidAssignment } from './rules/noInvalidAssignment.ts';
7+
import { noUnsupportedSyntax } from './rules/noUnsupportedSyntax.ts';
78

89
export const rules = {
910
'no-integer-division': noIntegerDivision,
1011
'no-unwrapped-objects': noUnwrappedObjects,
1112
'no-uninitialized-variables': noUninitializedVariables,
1213
'no-math': noMath,
1314
'no-invalid-assignment': noInvalidAssignment,
15+
'no-unsupported-syntax': noUnsupportedSyntax,
1416
} as const;
1517

1618
type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEntry>;
@@ -21,6 +23,7 @@ export const recommendedRules = {
2123
'typegpu/no-uninitialized-variables': 'error',
2224
'typegpu/no-math': 'warn',
2325
'typegpu/no-invalid-assignment': 'error',
26+
'typegpu/no-unsupported-syntax': 'error',
2427
} as const satisfies Rules;
2528

2629
export const allRules = {
@@ -29,4 +32,5 @@ export const allRules = {
2932
'typegpu/no-uninitialized-variables': 'error',
3033
'typegpu/no-math': 'error',
3134
'typegpu/no-invalid-assignment': 'error',
35+
'typegpu/no-unsupported-syntax': 'error',
3236
} as const satisfies Rules;

packages/eslint-plugin/src/enhancers/directiveTracking.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type FunctionNode =
99

1010
export type DirectiveData = {
1111
getEnclosingTypegpuFunction: () => FunctionNode | undefined;
12+
getDirectiveStack: () => readonly { node: FunctionNode; directives: string[] }[];
1213
};
1314

1415
/**
@@ -48,13 +49,16 @@ export const directiveTracking: RuleEnhancer<DirectiveData> = () => {
4849
return {
4950
visitors,
5051
state: {
51-
getEnclosingTypegpuFunction: () => {
52+
getEnclosingTypegpuFunction() {
5253
const current = stack.at(-1);
5354
if (current && current.directives.includes('use gpu')) {
5455
return current.node;
5556
}
5657
return undefined;
5758
},
59+
getDirectiveStack() {
60+
return stack;
61+
},
5862
},
5963
};
6064
};
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
import type { TSESTree } from '@typescript-eslint/utils';
2+
import { enhanceRule } from '../enhanceRule.ts';
3+
import { directiveTracking } from '../enhancers/directiveTracking.ts';
4+
import { createRule } from '../ruleCreator.ts';
5+
6+
export const noUnsupportedSyntax = createRule({
7+
name: 'no-unsupported-syntax',
8+
meta: {
9+
type: 'problem',
10+
docs: {
11+
description: `Disallow JS syntax that will not be parsed into valid WGSL.`,
12+
},
13+
messages: {
14+
unexpected:
15+
"'{{snippet}}' will not parse into valid WGSL because it uses unsupported syntax: {{syntax}}.",
16+
},
17+
schema: [],
18+
},
19+
defaultOptions: [],
20+
21+
create: enhanceRule({ directives: directiveTracking }, (context, state) => {
22+
const { directives } = state;
23+
24+
function report(node: TSESTree.Node, syntax: string) {
25+
context.report({
26+
node,
27+
messageId: 'unexpected',
28+
data: { snippet: context.sourceCode.getText(node), syntax },
29+
});
30+
}
31+
32+
return {
33+
ArrowFunctionExpression(node) {
34+
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
35+
report(node, 'arrow function');
36+
}
37+
},
38+
39+
AssignmentExpression(node) {
40+
if (!directives.getEnclosingTypegpuFunction()) {
41+
return;
42+
}
43+
if (unsupportedAssignmentOps.includes(node.operator)) {
44+
report(node, `assignment expression '${node.operator}'`);
45+
}
46+
},
47+
48+
AssignmentPattern(node) {
49+
if (!directives.getEnclosingTypegpuFunction()) {
50+
return;
51+
}
52+
report(node, 'assignment pattern (default parameter)');
53+
},
54+
55+
AwaitExpression(node) {
56+
if (!directives.getEnclosingTypegpuFunction()) {
57+
return;
58+
}
59+
report(node, 'await expression');
60+
},
61+
62+
BinaryExpression(node) {
63+
if (!directives.getEnclosingTypegpuFunction()) {
64+
return;
65+
}
66+
if (unsupportedBinaryOps.includes(node.operator)) {
67+
report(node, `binary operator '${node.operator}'`);
68+
}
69+
},
70+
71+
ChainExpression(node) {
72+
if (!directives.getEnclosingTypegpuFunction()) {
73+
return;
74+
}
75+
report(node, 'chain expression');
76+
},
77+
78+
ClassDeclaration(node) {
79+
if (!directives.getEnclosingTypegpuFunction()) {
80+
return;
81+
}
82+
report(node, 'class declaration');
83+
},
84+
85+
ClassExpression(node) {
86+
if (!directives.getEnclosingTypegpuFunction()) {
87+
return;
88+
}
89+
report(node, 'class expression');
90+
},
91+
92+
DoWhileStatement(node) {
93+
if (!directives.getEnclosingTypegpuFunction()) {
94+
return;
95+
}
96+
report(node, 'do-while loop');
97+
},
98+
99+
ForInStatement(node) {
100+
if (!directives.getEnclosingTypegpuFunction()) {
101+
return;
102+
}
103+
report(node, 'for-in loop');
104+
},
105+
106+
FunctionDeclaration(node) {
107+
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
108+
report(node, 'function declaration');
109+
}
110+
},
111+
112+
FunctionExpression(node) {
113+
if (directives.getDirectiveStack().at(-2)?.directives.includes('use gpu')) {
114+
report(node, 'function expression');
115+
}
116+
},
117+
118+
Literal(node) {
119+
if (!directives.getEnclosingTypegpuFunction()) {
120+
return;
121+
}
122+
if ('regex' in node && node.regex) {
123+
report(node, 'regular expression literal');
124+
}
125+
},
126+
127+
LogicalExpression(node) {
128+
if (!directives.getEnclosingTypegpuFunction()) {
129+
return;
130+
}
131+
if (node.operator === '??') {
132+
report(node, 'nullish coalescing');
133+
}
134+
},
135+
136+
NewExpression(node) {
137+
if (!directives.getEnclosingTypegpuFunction()) {
138+
return;
139+
}
140+
report(node, `'new' expression`);
141+
},
142+
143+
PrivateIdentifier(node) {
144+
if (!directives.getEnclosingTypegpuFunction()) {
145+
return;
146+
}
147+
report(node, 'private identifier');
148+
},
149+
150+
Property(node) {
151+
if (!directives.getEnclosingTypegpuFunction()) {
152+
return;
153+
}
154+
if (node.computed) {
155+
report(node, 'computed property key');
156+
}
157+
},
158+
159+
SequenceExpression(node) {
160+
if (!directives.getEnclosingTypegpuFunction()) {
161+
return;
162+
}
163+
report(node, 'sequence expression (comma operator)');
164+
},
165+
166+
SpreadElement(node) {
167+
if (!directives.getEnclosingTypegpuFunction()) {
168+
return;
169+
}
170+
report(node, 'spread element');
171+
},
172+
173+
SwitchStatement(node) {
174+
if (!directives.getEnclosingTypegpuFunction()) {
175+
return;
176+
}
177+
report(node, 'switch statement');
178+
},
179+
180+
TemplateLiteral(node) {
181+
if (!directives.getEnclosingTypegpuFunction()) {
182+
return;
183+
}
184+
report(node, 'template literal');
185+
},
186+
187+
ThrowStatement(node) {
188+
if (!directives.getEnclosingTypegpuFunction()) {
189+
return;
190+
}
191+
report(node, 'throw statement');
192+
},
193+
194+
TryStatement(node) {
195+
if (!directives.getEnclosingTypegpuFunction()) {
196+
return;
197+
}
198+
report(node, 'try-catch statement');
199+
},
200+
201+
UnaryExpression(node) {
202+
if (!directives.getEnclosingTypegpuFunction()) {
203+
return;
204+
}
205+
if (unsupportedUnaryOps.includes(node.operator)) {
206+
report(node, `unary operator '${node.operator}'`);
207+
}
208+
},
209+
210+
UpdateExpression(node) {
211+
if (!directives.getEnclosingTypegpuFunction()) {
212+
return;
213+
}
214+
if (node.prefix) {
215+
report(node, 'prefix update expression');
216+
}
217+
},
218+
219+
VariableDeclaration(node) {
220+
if (!directives.getEnclosingTypegpuFunction()) {
221+
return;
222+
}
223+
if (node.kind === 'var') {
224+
report(node, `'var' declaration`);
225+
}
226+
if (node.declarations.length > 1) {
227+
report(node, 'multiple variable declarations in one statement');
228+
}
229+
},
230+
231+
VariableDeclarator(node) {
232+
if (!directives.getEnclosingTypegpuFunction()) {
233+
return;
234+
}
235+
if (node.id.type !== 'Identifier') {
236+
report(node, 'variable declaration using destructuring');
237+
}
238+
},
239+
240+
YieldExpression(node) {
241+
if (!directives.getEnclosingTypegpuFunction()) {
242+
return;
243+
}
244+
report(node, 'yield expression');
245+
},
246+
};
247+
}),
248+
});
249+
250+
const unsupportedAssignmentOps = ['&&=', '**=', '||=', '>>>=', '??='];
251+
const unsupportedBinaryOps = ['==', '!=', '>>>', 'in', 'instanceof', '|>'];
252+
const unsupportedUnaryOps = ['+', 'typeof', 'void', 'delete'];

0 commit comments

Comments
 (0)