Skip to content

Commit 01f8d5c

Browse files
feat: Lint rule for invalid assignment (#2235)
1 parent cb79792 commit 01f8d5c

File tree

11 files changed

+386
-11
lines changed

11 files changed

+386
-11
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export default defineConfig([
4343
| Name                       | Description | 🚨 | ⚠️ |
4444
| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :- | :- |
4545
| [no-integer-division](docs/rules/no-integer-division.md) | Disallow division incorporating numbers wrapped in 'u32' and 'i32' | ||
46+
| [no-invalid-assignment](docs/rules/no-invalid-assignment.md) | Disallow assignments that will generate invalid WGSL || |
4647
| [no-math](docs/rules/no-math.md) | Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions | ||
4748
| [no-uninitialized-variables](docs/rules/no-uninitialized-variables.md) | Disallow variable declarations without initializers inside 'use gpu' functions || |
4849
| [no-unwrapped-objects](docs/rules/no-unwrapped-objects.md) | Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns) || |
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# typegpu/no-invalid-assignment
2+
3+
📝 Disallow assignments that will generate invalid 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 = (a) => {
15+
'use gpu';
16+
a = 1;
17+
}
18+
```
19+
```ts
20+
const fn = (a) => {
21+
'use gpu';
22+
a.prop++;
23+
}
24+
```
25+
```ts
26+
let a;
27+
const fn = () => {
28+
'use gpu';
29+
a = 1;
30+
}
31+
```
32+
33+
Examples of **correct** code for this rule:
34+
35+
```ts
36+
const fn = () => {
37+
'use gpu';
38+
const ref = d.ref(0);
39+
other(ref);
40+
};
41+
42+
const other = (ref: d.ref<number>) => {
43+
'use gpu';
44+
ref.$ = 1;
45+
};
46+
```
47+
```ts
48+
const privateVar = tgpu.privateVar(d.u32);
49+
const fn = () => {
50+
'use gpu';
51+
privateVar.$ = 1;
52+
}
53+
```
54+

packages/eslint-plugin/src/configs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ import { noIntegerDivision } from './rules/noIntegerDivision.ts';
33
import { noUnwrappedObjects } from './rules/noUnwrappedObjects.ts';
44
import { noMath } from './rules/noMath.ts';
55
import { noUninitializedVariables } from './rules/noUninitializedVariables.ts';
6+
import { noInvalidAssignment } from './rules/noInvalidAssignment.ts';
67

78
export const rules = {
89
'no-integer-division': noIntegerDivision,
910
'no-unwrapped-objects': noUnwrappedObjects,
1011
'no-uninitialized-variables': noUninitializedVariables,
1112
'no-math': noMath,
13+
'no-invalid-assignment': noInvalidAssignment,
1214
} as const;
1315

1416
type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEntry>;
@@ -18,11 +20,13 @@ export const recommendedRules: Rules = {
1820
'typegpu/no-unwrapped-objects': 'error',
1921
'typegpu/no-uninitialized-variables': 'error',
2022
'typegpu/no-math': 'warn',
23+
'typegpu/no-invalid-assignment': 'error',
2124
};
2225

2326
export const allRules: Rules = {
2427
'typegpu/no-integer-division': 'error',
2528
'typegpu/no-unwrapped-objects': 'error',
2629
'typegpu/no-uninitialized-variables': 'error',
2730
'typegpu/no-math': 'error',
31+
'typegpu/no-invalid-assignment': 'error',
2832
};

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@ import type { TSESTree } from '@typescript-eslint/utils';
22
import type { RuleListener } from '@typescript-eslint/utils/ts-eslint';
33
import type { RuleEnhancer } from '../enhanceRule.ts';
44

5+
export type FunctionNode =
6+
| TSESTree.FunctionDeclaration
7+
| TSESTree.FunctionExpression
8+
| TSESTree.ArrowFunctionExpression;
9+
510
export type DirectiveData = {
6-
insideUseGpu: () => boolean;
11+
getEnclosingTypegpuFunction: () => FunctionNode | undefined;
712
};
813

914
/**
@@ -16,17 +21,17 @@ export type DirectiveData = {
1621
* - top level directives.
1722
*/
1823
export const directiveTracking: RuleEnhancer<DirectiveData> = () => {
19-
const stack: string[][] = [];
24+
const stack: { node: FunctionNode; directives: string[] }[] = [];
2025

2126
const visitors: RuleListener = {
2227
FunctionDeclaration(node) {
23-
stack.push(getDirectives(node));
28+
stack.push({ node, directives: getDirectives(node) });
2429
},
2530
FunctionExpression(node) {
26-
stack.push(getDirectives(node));
31+
stack.push({ node, directives: getDirectives(node) });
2732
},
2833
ArrowFunctionExpression(node) {
29-
stack.push(getDirectives(node));
34+
stack.push({ node, directives: getDirectives(node) });
3035
},
3136

3237
'FunctionDeclaration:exit'() {
@@ -42,7 +47,15 @@ export const directiveTracking: RuleEnhancer<DirectiveData> = () => {
4247

4348
return {
4449
visitors,
45-
state: { insideUseGpu: () => (stack.at(-1) ?? []).includes('use gpu') },
50+
state: {
51+
getEnclosingTypegpuFunction: () => {
52+
const current = stack.at(-1);
53+
if (current && current.directives.includes('use gpu')) {
54+
return current.node;
55+
}
56+
return undefined;
57+
},
58+
},
4659
};
4760
};
4861

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { ASTUtils, type TSESTree } from '@typescript-eslint/utils';
2+
import { createRule } from '../ruleCreator.ts';
3+
import { enhanceRule } from '../enhanceRule.ts';
4+
import { directiveTracking } from '../enhancers/directiveTracking.ts';
5+
import type { RuleContext } from '@typescript-eslint/utils/ts-eslint';
6+
7+
export const noInvalidAssignment = createRule({
8+
name: 'no-invalid-assignment',
9+
meta: {
10+
type: 'problem',
11+
docs: {
12+
description: `Disallow assignments that will generate invalid WGSL`,
13+
},
14+
messages: {
15+
parameterAssignment:
16+
"Cannot assign to '{{snippet}}' since WGSL parameters are immutable. If you're using d.ref, please either use '.$' or disable this rule",
17+
jsAssignment:
18+
"Cannot assign to '{{snippet}}' since it is a JS variable defined outside of the current TypeGPU function's scope. Use buffers, workgroup variables or local variables instead",
19+
},
20+
schema: [],
21+
},
22+
defaultOptions: [],
23+
24+
create: enhanceRule({ directives: directiveTracking }, (context, state) => {
25+
const { directives } = state;
26+
27+
return {
28+
UpdateExpression(node) {
29+
const enclosingFn = directives.getEnclosingTypegpuFunction();
30+
validateAssignment(context, node, enclosingFn, node.argument);
31+
},
32+
33+
AssignmentExpression(node) {
34+
const enclosingFn = directives.getEnclosingTypegpuFunction();
35+
validateAssignment(context, node, enclosingFn, node.left);
36+
},
37+
};
38+
}),
39+
});
40+
41+
function validateAssignment(
42+
context: Readonly<RuleContext<'parameterAssignment' | 'jsAssignment', []>>,
43+
node: TSESTree.Node,
44+
enclosingFn: TSESTree.Node | undefined,
45+
leftNode: TSESTree.Node,
46+
) {
47+
if (!enclosingFn) {
48+
return;
49+
}
50+
51+
// follow the member expression chain
52+
let assignee = leftNode;
53+
while (assignee.type === 'MemberExpression') {
54+
if (assignee.property.type === 'Identifier' && assignee.property.name === '$') {
55+
// a dollar was used so we assume this assignment is fine
56+
return;
57+
}
58+
assignee = assignee.object;
59+
}
60+
if (assignee.type !== 'Identifier') {
61+
return;
62+
}
63+
64+
// look for a scope that defines the variable
65+
const variable = ASTUtils.findVariable(context.sourceCode.getScope(assignee), assignee);
66+
// defs is an array because there may be multiple definitions with `var`
67+
const def = variable?.defs[0];
68+
69+
// check if variable is global or was defined outside of current function by checking ranges
70+
// NOTE: if the variable is an outer function parameter, then the enclosingFn range will be encompassed by node range
71+
if (
72+
!def ||
73+
(def && (def.node.range[0] < enclosingFn.range[0] || enclosingFn.range[1] < def.node.range[1]))
74+
) {
75+
context.report({
76+
messageId: 'jsAssignment',
77+
node,
78+
data: { snippet: context.sourceCode.getText(leftNode) },
79+
});
80+
return;
81+
}
82+
83+
if (def.type === 'Parameter') {
84+
context.report({
85+
messageId: 'parameterAssignment',
86+
node,
87+
data: { snippet: context.sourceCode.getText(leftNode) },
88+
});
89+
}
90+
}

packages/eslint-plugin/src/rules/noMath.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const noMath = createRule({
2424

2525
return {
2626
CallExpression(node) {
27-
if (!directives.insideUseGpu()) {
27+
if (!directives.getEnclosingTypegpuFunction()) {
2828
return;
2929
}
3030

packages/eslint-plugin/src/rules/noUninitializedVariables.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const noUninitializedVariables = createRule({
2121

2222
return {
2323
VariableDeclarator(node) {
24-
if (!directives.insideUseGpu()) {
24+
if (!directives.getEnclosingTypegpuFunction()) {
2525
return;
2626
}
2727
if (node.parent?.parent?.type === 'ForOfStatement') {

packages/eslint-plugin/src/rules/noUnwrappedObjects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const noUnwrappedObjects = createRule({
2222

2323
return {
2424
ObjectExpression(node) {
25-
if (!directives.insideUseGpu()) {
25+
if (!directives.getEnclosingTypegpuFunction()) {
2626
return;
2727
}
2828
let parent = getNonTransparentParent(node);

0 commit comments

Comments
 (0)