diff --git a/packages/eslint-plugin/src/configs.ts b/packages/eslint-plugin/src/configs.ts index 52ffdb996d..383b7b87e6 100644 --- a/packages/eslint-plugin/src/configs.ts +++ b/packages/eslint-plugin/src/configs.ts @@ -1,10 +1,14 @@ import type { TSESLint } from '@typescript-eslint/utils'; import { integerDivision } from './rules/integerDivision.ts'; import { unwrappedPojos } from './rules/unwrappedPojos.ts'; +import { math } from './rules/math.ts'; +import { uninitializedVariable } from './rules/uninitializedVariable.ts'; export const rules = { 'integer-division': integerDivision, 'unwrapped-pojo': unwrappedPojos, + 'uninitialized-variable': uninitializedVariable, + math: math, } as const; type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEntry>; @@ -12,9 +16,13 @@ type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEnt export const recommendedRules: Rules = { 'typegpu/integer-division': 'warn', 'typegpu/unwrapped-pojo': 'warn', + 'typegpu/uninitialized-variable': 'warn', + 'typegpu/math': 'warn', }; export const allRules: Rules = { 'typegpu/integer-division': 'error', 'typegpu/unwrapped-pojo': 'error', + 'typegpu/uninitialized-variable': 'error', + 'typegpu/math': 'error', }; diff --git a/packages/eslint-plugin/src/rules/math.ts b/packages/eslint-plugin/src/rules/math.ts new file mode 100644 index 0000000000..b0b0d74eb2 --- /dev/null +++ b/packages/eslint-plugin/src/rules/math.ts @@ -0,0 +1,42 @@ +import { createRule } from '../ruleCreator.ts'; +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; + +export const math = createRule({ + name: 'math', + meta: { + type: 'suggestion', + docs: { + description: `Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions; use 'std' instead.`, + }, + messages: { + math: "Using Math methods, such as '{{snippet}}', is not advised, and may not work as expected. Use 'std' instead.", + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + CallExpression(node) { + if (!directives.insideUseGpu()) { + return; + } + + if ( + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'Math' + ) { + context.report({ + node, + messageId: 'math', + data: { snippet: context.sourceCode.getText(node) }, + }); + } + }, + }; + }), +}); diff --git a/packages/eslint-plugin/src/rules/uninitializedVariable.ts b/packages/eslint-plugin/src/rules/uninitializedVariable.ts new file mode 100644 index 0000000000..59b85981ba --- /dev/null +++ b/packages/eslint-plugin/src/rules/uninitializedVariable.ts @@ -0,0 +1,41 @@ +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; +import { createRule } from '../ruleCreator.ts'; + +export const uninitializedVariable = createRule({ + name: 'uninitialized-variable', + meta: { + type: 'problem', + docs: { + description: `Always assign an initial value when declaring a variable inside TypeGPU functions.`, + }, + messages: { + uninitializedVariable: "'{{snippet}}' should have an initial value.", + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + VariableDeclarator(node) { + if (!directives.insideUseGpu()) { + return; + } + if (node.parent?.parent?.type === 'ForOfStatement') { + // one exception where we allow uninitialized variable + return; + } + if (node.init === null) { + context.report({ + node, + messageId: 'uninitializedVariable', + data: { snippet: context.sourceCode.getText(node) }, + }); + } + }, + }; + }), +}); diff --git a/packages/eslint-plugin/tests/math.test.ts b/packages/eslint-plugin/tests/math.test.ts new file mode 100644 index 0000000000..c124102882 --- /dev/null +++ b/packages/eslint-plugin/tests/math.test.ts @@ -0,0 +1,24 @@ +import { describe } from 'vitest'; +import { ruleTester } from './ruleTester.ts'; +import { math } from '../src/rules/math.ts'; + +describe('math', () => { + ruleTester.run('math', math, { + valid: [ + 'const result = Math.sin(1);', + 'const t = std.sin(Math.PI)', + "const fn = () => { 'use gpu'; const vec = std.sin(Math.PI); }", + ], + invalid: [ + { + code: "const fn = () => { 'use gpu'; const vec = Math.sin(0); }", + errors: [ + { + messageId: 'math', + data: { snippet: 'Math.sin(0)' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin/tests/uninitializedVariable.test.ts b/packages/eslint-plugin/tests/uninitializedVariable.test.ts new file mode 100644 index 0000000000..d35b715fea --- /dev/null +++ b/packages/eslint-plugin/tests/uninitializedVariable.test.ts @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { ruleTester } from './ruleTester.ts'; +import { uninitializedVariable } from '../src/rules/uninitializedVariable.ts'; + +describe('uninitializedVariable', () => { + ruleTester.run('uninitializedVariable', uninitializedVariable, { + valid: [ + 'let a;', + 'let a, b;', + "const fn = () => { 'use gpu'; const vec = d.vec3f(); }", + "const fn = () => { 'use gpu'; let vec = d.vec3f(); }", + `const fn = () => { 'use gpu'; + let a = 0; + for (const foo of tgpu.unroll([1, 2, 3])) { + a += foo; + } + }`, + ], + invalid: [ + { + code: "const fn = () => { 'use gpu'; let vec; }", + errors: [ + { + messageId: 'uninitializedVariable', + data: { snippet: 'vec' }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; let a = 1, b, c = d.vec3f(), d; }", + errors: [ + { + messageId: 'uninitializedVariable', + data: { snippet: 'b' }, + }, + { + messageId: 'uninitializedVariable', + data: { snippet: 'd' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/typegpu/tests/jsMath.test.ts b/packages/typegpu/tests/jsMath.test.ts index 25529a30f3..92bcdcd9e9 100644 --- a/packages/typegpu/tests/jsMath.test.ts +++ b/packages/typegpu/tests/jsMath.test.ts @@ -19,6 +19,7 @@ describe('Math', () => { const myFn = () => { 'use gpu'; const a = 0.5; + // oxlint-disable-next-line typegpu/math const b = Math.sin(a); }; @@ -33,6 +34,7 @@ describe('Math', () => { it('precomputes Math.sin when applicable', () => { const myFn = () => { 'use gpu'; + // oxlint-disable-next-line typegpu/math const a = Math.sin(0.5); }; @@ -47,6 +49,7 @@ describe('Math', () => { const myFn = () => { 'use gpu'; const a = d.u32(); + // oxlint-disable-next-line typegpu/math const b = Math.sin(a); }; @@ -62,6 +65,7 @@ describe('Math', () => { const myFn = () => { 'use gpu'; const a = d.u32(); + // oxlint-disable-next-line typegpu/math const b = Math.min(a, 1, 2, 3); }; @@ -76,6 +80,7 @@ describe('Math', () => { it('throws a readable error when unsupported Math feature is used', () => { const myFn = () => { 'use gpu'; + // oxlint-disable-next-line typegpu/math const a = Math.log1p(1); }; @@ -90,6 +95,7 @@ describe('Math', () => { it('correctly applies Math.fround', () => { const myFn = () => { 'use gpu'; + // oxlint-disable-next-line typegpu/math const a = Math.fround(16777217); };