From c9f0366e49492c5cd92f0d91a00a2a0d545fa1e4 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:32:58 +0200 Subject: [PATCH 01/12] Duplicate eslint plugin --- .../.eslint-doc-generatorrc.mjs | 9 + packages/eslint-plugin-internal/README.md | 51 +++++ .../docs/rules/no-integer-division.md | 34 +++ .../docs/rules/no-invalid-assignment.md | 54 +++++ .../docs/rules/no-math.md | 31 +++ .../docs/rules/no-uninitialized-variables.md | 31 +++ .../docs/rules/no-unwrapped-objects.md | 38 ++++ packages/eslint-plugin-internal/package.json | 65 ++++++ .../eslint-plugin-internal/src/configs.ts | 32 +++ .../eslint-plugin-internal/src/enhanceRule.ts | 80 +++++++ .../src/enhancers/directiveTracking.ts | 83 +++++++ packages/eslint-plugin-internal/src/index.ts | 33 +++ .../eslint-plugin-internal/src/nodeHelpers.ts | 20 ++ .../eslint-plugin-internal/src/ruleCreator.ts | 6 + .../src/rules/noIntegerDivision.ts | 61 +++++ .../src/rules/noInvalidAssignment.ts | 90 ++++++++ .../src/rules/noMath.ts | 57 +++++ .../src/rules/noUninitializedVariables.ts | 41 ++++ .../src/rules/noUnwrappedObjects.ts | 49 ++++ .../tests/ruleNames.test.ts | 8 + .../tests/rules/noIntegerDivision.test.ts | 45 ++++ .../tests/rules/noInvalidAssignment.test.ts | 210 ++++++++++++++++++ .../tests/rules/noMath.test.ts | 26 +++ .../rules/noUninitializedVariables.test.ts | 44 ++++ .../tests/rules/noUnwrappedObjects.test.ts | 79 +++++++ .../tests/utils/ruleTester.ts | 10 + packages/eslint-plugin-internal/tsconfig.json | 8 + .../eslint-plugin-internal/tsdown.config.ts | 9 + .../eslint-plugin-internal/vitest.config.ts | 8 + 29 files changed, 1312 insertions(+) create mode 100644 packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs create mode 100644 packages/eslint-plugin-internal/README.md create mode 100644 packages/eslint-plugin-internal/docs/rules/no-integer-division.md create mode 100644 packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md create mode 100644 packages/eslint-plugin-internal/docs/rules/no-math.md create mode 100644 packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md create mode 100644 packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md create mode 100644 packages/eslint-plugin-internal/package.json create mode 100644 packages/eslint-plugin-internal/src/configs.ts create mode 100644 packages/eslint-plugin-internal/src/enhanceRule.ts create mode 100644 packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts create mode 100644 packages/eslint-plugin-internal/src/index.ts create mode 100644 packages/eslint-plugin-internal/src/nodeHelpers.ts create mode 100644 packages/eslint-plugin-internal/src/ruleCreator.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noMath.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts create mode 100644 packages/eslint-plugin-internal/tests/ruleNames.test.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noMath.test.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts create mode 100644 packages/eslint-plugin-internal/tests/utils/ruleTester.ts create mode 100644 packages/eslint-plugin-internal/tsconfig.json create mode 100644 packages/eslint-plugin-internal/tsdown.config.ts create mode 100644 packages/eslint-plugin-internal/vitest.config.ts diff --git a/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs b/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs new file mode 100644 index 0000000000..0604c2e099 --- /dev/null +++ b/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs @@ -0,0 +1,9 @@ +const config = { + ignoreConfig: ['all'], + configEmoji: [['recommended', '⭐']], + postprocess: (content) => { + return content.replaceAll('💼', '🚨').replaceAll('🚫', '💤'); + }, +}; + +export default config; diff --git a/packages/eslint-plugin-internal/README.md b/packages/eslint-plugin-internal/README.md new file mode 100644 index 0000000000..fae1227e74 --- /dev/null +++ b/packages/eslint-plugin-internal/README.md @@ -0,0 +1,51 @@ +
+ +# eslint-plugin-typegpu + +TypeGPU specific linting rules for ESLint. + +[Docs](https://docs.swmansion.com/TypeGPU/tooling/eslint-plugin-typegpu/) -- [GitHub](https://github.com/software-mansion/TypeGPU/tree/main/packages/eslint-plugin) -- [npm](https://www.npmjs.com/package/eslint-plugin-typegpu) + +
+ +## Installation + +`npm add -D eslint-plugin-typegpu` + +After installing, the plugin needs to be configured. + +## Configuration + +Configuration depends on the linter used. + +In eslint, either define the used rules manually, or use one of the configs provided by the plugin. + +```ts +import { defineConfig } from "eslint/config"; +import typegpu from "eslint-plugin-typegpu"; + +export default defineConfig([ +// other configs + typegpu.configs.recommended, +]); +``` + +`eslint-plugin-typegpu` provides two configs: `all` (enabled on all rules) and `recommended`. + +## List of supported rules + + + +🚨 Configurations enabled in.\ +⚠️ Configurations set to warn in.\ +⭐ Set in the `recommended` configuration. + +| Name                       | Description | 🚨 | ⚠️ | +| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :- | :- | +| [no-integer-division](docs/rules/no-integer-division.md) | Disallow division incorporating numbers wrapped in 'u32' and 'i32' | | ⭐ | +| [no-invalid-assignment](docs/rules/no-invalid-assignment.md) | Disallow assignments that will generate invalid WGSL | ⭐ | | +| [no-math](docs/rules/no-math.md) | Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions | | ⭐ | +| [no-uninitialized-variables](docs/rules/no-uninitialized-variables.md) | Disallow variable declarations without initializers inside 'use gpu' functions | ⭐ | | +| [no-unwrapped-objects](docs/rules/no-unwrapped-objects.md) | Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns) | ⭐ | | + + diff --git a/packages/eslint-plugin-internal/docs/rules/no-integer-division.md b/packages/eslint-plugin-internal/docs/rules/no-integer-division.md new file mode 100644 index 0000000000..4c934c6282 --- /dev/null +++ b/packages/eslint-plugin-internal/docs/rules/no-integer-division.md @@ -0,0 +1,34 @@ +# typegpu/no-integer-division + +📝 Disallow division incorporating numbers wrapped in 'u32' and 'i32'. + +⚠️ This rule _warns_ in the ⭐ `recommended` config. + + + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +const a = d.u32(1) / d.u32(2); +``` +```ts +const a = 1 / d.u32(2); +``` +```ts +const a = 1 / d.i32(2); +``` + +Examples of **correct** code for this rule: + +```ts +const a = 1 / 2; +``` +```ts +const a = d.u32(d.u32(1) / d.u32(2)); +``` + +Note that this rule is not type aware. +Extracting the dividend and the divisor to variables will silence the rule, +but it will not make the code behave differently. \ No newline at end of file diff --git a/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md b/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md new file mode 100644 index 0000000000..5fb48326f4 --- /dev/null +++ b/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md @@ -0,0 +1,54 @@ +# typegpu/no-invalid-assignment + +📝 Disallow assignments that will generate invalid WGSL. + +🚨 This rule is enabled in the ⭐ `recommended` config. + + + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +const fn = (a) => { + 'use gpu'; + a = 1; +} +``` +```ts +const fn = (a) => { + 'use gpu'; + a.prop++; +} +``` +```ts +let a; +const fn = () => { + 'use gpu'; + a = 1; +} +``` + +Examples of **correct** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + const ref = d.ref(0); + other(ref); +}; + +const other = (ref: d.ref) => { + 'use gpu'; + ref.$ = 1; +}; +``` +```ts +const privateVar = tgpu.privateVar(d.u32); +const fn = () => { + 'use gpu'; + privateVar.$ = 1; +} +``` + diff --git a/packages/eslint-plugin-internal/docs/rules/no-math.md b/packages/eslint-plugin-internal/docs/rules/no-math.md new file mode 100644 index 0000000000..c136e5d04c --- /dev/null +++ b/packages/eslint-plugin-internal/docs/rules/no-math.md @@ -0,0 +1,31 @@ +# typegpu/no-math + +📝 Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions. + +⚠️ This rule _warns_ in the ⭐ `recommended` config. + + + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + const vec = Math.sin(0); +} +``` + +Examples of **correct** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + const a = std.sin(Math.PI); +} +``` +```ts +// outside 'use gpu' +const a = Math.sin(1); +``` \ No newline at end of file diff --git a/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md b/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md new file mode 100644 index 0000000000..f272ab93b9 --- /dev/null +++ b/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md @@ -0,0 +1,31 @@ +# typegpu/no-uninitialized-variables + +📝 Disallow variable declarations without initializers inside 'use gpu' functions. + +🚨 This rule is enabled in the ⭐ `recommended` config. + + + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + let a; +} +``` + +Examples of **correct** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + let vec = d.vec3f(); +} +``` +```ts +// outside 'use gpu' +let a; +``` diff --git a/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md b/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md new file mode 100644 index 0000000000..bcdeb7e20c --- /dev/null +++ b/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md @@ -0,0 +1,38 @@ +# typegpu/no-unwrapped-objects + +📝 Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns). + +🚨 This rule is enabled in the ⭐ `recommended` config. + + + +## Rule details + +Examples of **incorrect** code for this rule: + +```ts +const fn = () => { + 'use gpu'; + const unwrapped = { a: 1 }; +} +``` + +Examples of **correct** code for this rule: + +```ts +const pojo = { a: 1 }; +``` +```ts +const fn = () => { + 'use gpu'; + return { a: 1 }; +} +``` +```ts +const Schema = d.struct({ a: d.u32 }); + +const fn = () => { + 'use gpu'; + const wrapped = Schema({ a: 1 }); +} +``` \ No newline at end of file diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json new file mode 100644 index 0000000000..003192a77f --- /dev/null +++ b/packages/eslint-plugin-internal/package.json @@ -0,0 +1,65 @@ +{ + "name": "eslint-plugin-typegpu", + "version": "0.10.0-alpha.2", + "description": "TypeGPU specific linting rules for ESLint", + "keywords": [ + "eslint", + "eslint-plugin", + "eslintplugin", + "typegpu", + "use gpu" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/software-mansion/TypeGPU.git#main", + "directory": "packages/eslint-plugin" + }, + "files": [ + "dist" + ], + "type": "module", + "sideEffects": false, + "main": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "publishConfig": { + "exports": { + ".": { + "types": "./dist/index.d.mts", + "import": "./dist/index.mjs" + } + }, + "main": "./dist/index.mjs", + "types": "./dist/index.d.mts" + }, + "scripts": { + "build": "tsdown", + "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", + "docs:init": "eslint-doc-generator --init-rule-docs", + "docs:check": "eslint-doc-generator --check", + "docs:update": "eslint-doc-generator", + "test": "vitest", + "prepublishOnly": "pnpm run docs:check && pnpm run test:types && vitest run && pnpm run build" + }, + "dependencies": { + "@typescript-eslint/utils": "^8.57.2" + }, + "devDependencies": { + "@types/node": "catalog:types", + "@typescript-eslint/rule-tester": "^8.57.2", + "eslint": "^9.39.2", + "eslint-doc-generator": "^3.3.2", + "tsdown": "^0.20.3", + "typescript": "^5.9.3", + "vitest": "^4.0.17" + }, + "peerDependencies": { + "eslint": "^9.0.0" + }, + "packageManager": "pnpm@10.15.1" +} diff --git a/packages/eslint-plugin-internal/src/configs.ts b/packages/eslint-plugin-internal/src/configs.ts new file mode 100644 index 0000000000..df130b1dcd --- /dev/null +++ b/packages/eslint-plugin-internal/src/configs.ts @@ -0,0 +1,32 @@ +import type { TSESLint } from '@typescript-eslint/utils'; +import { noIntegerDivision } from './rules/noIntegerDivision.ts'; +import { noUnwrappedObjects } from './rules/noUnwrappedObjects.ts'; +import { noMath } from './rules/noMath.ts'; +import { noUninitializedVariables } from './rules/noUninitializedVariables.ts'; +import { noInvalidAssignment } from './rules/noInvalidAssignment.ts'; + +export const rules = { + 'no-integer-division': noIntegerDivision, + 'no-unwrapped-objects': noUnwrappedObjects, + 'no-uninitialized-variables': noUninitializedVariables, + 'no-math': noMath, + 'no-invalid-assignment': noInvalidAssignment, +} as const; + +type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEntry>; + +export const recommendedRules: Rules = { + 'typegpu/no-integer-division': 'warn', + 'typegpu/no-unwrapped-objects': 'error', + 'typegpu/no-uninitialized-variables': 'error', + 'typegpu/no-math': 'warn', + 'typegpu/no-invalid-assignment': 'error', +}; + +export const allRules: Rules = { + 'typegpu/no-integer-division': 'error', + 'typegpu/no-unwrapped-objects': 'error', + 'typegpu/no-uninitialized-variables': 'error', + 'typegpu/no-math': 'error', + 'typegpu/no-invalid-assignment': 'error', +}; diff --git a/packages/eslint-plugin-internal/src/enhanceRule.ts b/packages/eslint-plugin-internal/src/enhanceRule.ts new file mode 100644 index 0000000000..5d0a366b68 --- /dev/null +++ b/packages/eslint-plugin-internal/src/enhanceRule.ts @@ -0,0 +1,80 @@ +import type { RuleContext, RuleListener } from '@typescript-eslint/utils/ts-eslint'; + +export type RuleEnhancer = (context: RuleContext) => { + visitors: RuleListener; + state: TState; +}; + +type State>> = { + [K in keyof TMap]: TMap[K] extends RuleEnhancer ? S : never; +}; + +/** + * Allows enhancing rule code with additional context provided by RuleEnhancers (reusable node visitors collecting data). + * @param enhancers a record of RuleEnhancers + * @param rule a visitor with an additional `state` argument that allows access to the enhancers' data + * @returns a resulting `(context: Context) => RuleListener` function + * + * @example + * // inside of `createRule` + * create: enhanceRule({ metadata: metadataTrackingEnhancer }, (context, state) => { + * const { metadata } = state; + * + * return { + * ObjectExpression(node) { + * if (metadata.shouldReport()) { + * context.report({ node, messageId: 'error' }); + * } + * }, + * }; + */ +export function enhanceRule< + TMap extends Record>, + Context extends RuleContext, +>(enhancers: TMap, rule: (context: Context, state: State) => RuleListener) { + return (context: Context) => { + const enhancerVisitors: RuleListener[] = []; + const combinedState: Record = {}; + + for (const [key, enhancer] of Object.entries(enhancers)) { + const initializedEnhancer = enhancer(context); + enhancerVisitors.push(initializedEnhancer.visitors); + combinedState[key] = initializedEnhancer.state; + } + + const initializedRule = rule(context, combinedState as State); + + return mergeVisitors([...enhancerVisitors, initializedRule]); + }; +} + +/** + * Merges all passed visitors into one visitor. + * Retains visitor order: + * - on node enter, visitors are called in `visitorsList` order, + * - on node exit, visitors are called in reversed order. + */ +function mergeVisitors(visitors: RuleListener[]): RuleListener { + const merged: RuleListener = {}; + + const allKeys = new Set(visitors.flatMap((v) => Object.keys(v))); + + for (const key of allKeys) { + const listeners = visitors.map((v) => v[key]).filter((fn) => fn !== undefined); + + if (listeners.length === 0) { + continue; + } + + // Reverse order if node is an exit node + if (key.endsWith(':exit')) { + listeners.reverse(); + } + + merged[key] = (...args: unknown[]) => { + listeners.forEach((fn) => (fn as (...args: unknown[]) => void)(...args)); + }; + } + + return merged; +} diff --git a/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts b/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts new file mode 100644 index 0000000000..b490760afb --- /dev/null +++ b/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts @@ -0,0 +1,83 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import type { RuleListener } from '@typescript-eslint/utils/ts-eslint'; +import type { RuleEnhancer } from '../enhanceRule.ts'; + +export type FunctionNode = + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression; + +export type DirectiveData = { + getEnclosingTypegpuFunction: () => FunctionNode | undefined; +}; + +/** + * A RuleEnhancer that tracks whether the current node is inside a 'use gpu' function. + * + * @privateRemarks + * Should the need arise, the API could be updated to expose: + * - a list of directives of the current function, + * - directives of other visited functions, + * - top level directives. + */ +export const directiveTracking: RuleEnhancer = () => { + const stack: { node: FunctionNode; directives: string[] }[] = []; + + const visitors: RuleListener = { + FunctionDeclaration(node) { + stack.push({ node, directives: getDirectives(node) }); + }, + FunctionExpression(node) { + stack.push({ node, directives: getDirectives(node) }); + }, + ArrowFunctionExpression(node) { + stack.push({ node, directives: getDirectives(node) }); + }, + + 'FunctionDeclaration:exit'() { + stack.pop(); + }, + 'FunctionExpression:exit'() { + stack.pop(); + }, + 'ArrowFunctionExpression:exit'() { + stack.pop(); + }, + }; + + return { + visitors, + state: { + getEnclosingTypegpuFunction: () => { + const current = stack.at(-1); + if (current && current.directives.includes('use gpu')) { + return current.node; + } + return undefined; + }, + }, + }; +}; + +function getDirectives( + node: + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression, +): string[] { + const body = node.body; + if (body.type !== 'BlockStatement') { + return []; + } + + const directives: string[] = []; + for (const statement of body.body) { + if (statement.type === 'ExpressionStatement' && statement.directive) { + directives.push(statement.directive); + } else { + break; + } + } + + return directives; +} diff --git a/packages/eslint-plugin-internal/src/index.ts b/packages/eslint-plugin-internal/src/index.ts new file mode 100644 index 0000000000..08763811d3 --- /dev/null +++ b/packages/eslint-plugin-internal/src/index.ts @@ -0,0 +1,33 @@ +import pkg from '../package.json' with { type: 'json' }; +import type { TSESLint } from '@typescript-eslint/utils'; +import { allRules, recommendedRules, rules } from './configs.ts'; + +const pluginBase: TSESLint.FlatConfig.Plugin = { + meta: { + name: pkg.name, + version: pkg.version, + }, + rules, +}; + +const recommended: TSESLint.FlatConfig.Config = { + name: 'typegpu/recommended', + plugins: { typegpu: pluginBase }, + rules: recommendedRules, +}; + +const all: TSESLint.FlatConfig.Config = { + name: 'typegpu/all', + plugins: { typegpu: pluginBase }, + rules: allRules, +}; + +const plugin: TSESLint.FlatConfig.Plugin = { + ...pluginBase, + configs: { + recommended, + all, + }, +}; + +export default plugin; diff --git a/packages/eslint-plugin-internal/src/nodeHelpers.ts b/packages/eslint-plugin-internal/src/nodeHelpers.ts new file mode 100644 index 0000000000..4122427d17 --- /dev/null +++ b/packages/eslint-plugin-internal/src/nodeHelpers.ts @@ -0,0 +1,20 @@ +import { TSESTree } from '@typescript-eslint/utils'; + +const transparentNodes = [ + 'TSAsExpression', + 'TSSatisfiesExpression', + 'TSTypeAssertion', + 'TSNonNullExpression', +]; + +export function isTransparent(node: TSESTree.Node): boolean { + return transparentNodes.includes(node.type); +} + +export function getNonTransparentParent(node: TSESTree.Node) { + let parent = node.parent; + while (parent && isTransparent(parent)) { + parent = parent.parent; + } + return parent; +} diff --git a/packages/eslint-plugin-internal/src/ruleCreator.ts b/packages/eslint-plugin-internal/src/ruleCreator.ts new file mode 100644 index 0000000000..6d07477ffc --- /dev/null +++ b/packages/eslint-plugin-internal/src/ruleCreator.ts @@ -0,0 +1,6 @@ +import { ESLintUtils } from '@typescript-eslint/utils'; + +export const createRule = ESLintUtils.RuleCreator( + // TODO: docs for lint rules + () => `https://docs.swmansion.com/TypeGPU/getting-started/`, +); diff --git a/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts b/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts new file mode 100644 index 0000000000..99664f29c7 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts @@ -0,0 +1,61 @@ +import type { TSESTree } from '@typescript-eslint/utils'; +import { createRule } from '../ruleCreator.ts'; + +// TODO: detect `std.div(d.u32(1), d.u32(2))` +export const noIntegerDivision = createRule({ + name: 'no-integer-division', + meta: { + type: 'suggestion', + docs: { description: `Disallow division incorporating numbers wrapped in 'u32' and 'i32'` }, + messages: { + suspiciousDivision: + "'{{snippet}}' might result in floating point values. To perform integer division, wrap the result in 'd.u32' or 'd.i32' instead", + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + BinaryExpression(node) { + if (node.operator !== '/') { + return; + } + + if (node.parent?.type === 'CallExpression' && isIntCast(node.parent)) { + return; + } + + if (isIntCast(node.left) || isIntCast(node.right)) { + context.report({ + node, + messageId: 'suspiciousDivision', + data: { snippet: context.sourceCode.getText(node) }, + }); + } + }, + }; + }, +}); + +/** + * Checks if a node is a call expression to an integer cast function (i32 or u32). + * + * @example + * // for simplicity, using code snippets instead of ASTs + * isIntCasts('d.u32()'); // true + * isIntCasts('i32()'); // true + * isIntCasts('f32()'); // false + */ +function isIntCast(node: TSESTree.Expression): boolean { + if (node.type !== 'CallExpression') { + return false; + } + + let callee: TSESTree.Node = node.callee; + while (callee.type === 'MemberExpression') { + callee = callee.property; + } + + return callee.type === 'Identifier' && ['i32', 'u32'].includes(callee.name); +} diff --git a/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts b/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts new file mode 100644 index 0000000000..80d4cbf278 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts @@ -0,0 +1,90 @@ +import { ASTUtils, type TSESTree } from '@typescript-eslint/utils'; +import { createRule } from '../ruleCreator.ts'; +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +export const noInvalidAssignment = createRule({ + name: 'no-invalid-assignment', + meta: { + type: 'problem', + docs: { + description: `Disallow assignments that will generate invalid WGSL`, + }, + messages: { + parameterAssignment: + "Cannot assign to '{{snippet}}' since WGSL parameters are immutable. If you're using d.ref, please either use '.$' or disable this rule", + jsAssignment: + "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", + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + UpdateExpression(node) { + const enclosingFn = directives.getEnclosingTypegpuFunction(); + validateAssignment(context, node, enclosingFn, node.argument); + }, + + AssignmentExpression(node) { + const enclosingFn = directives.getEnclosingTypegpuFunction(); + validateAssignment(context, node, enclosingFn, node.left); + }, + }; + }), +}); + +function validateAssignment( + context: Readonly>, + node: TSESTree.Node, + enclosingFn: TSESTree.Node | undefined, + leftNode: TSESTree.Node, +) { + if (!enclosingFn) { + return; + } + + // follow the member expression chain + let assignee = leftNode; + while (assignee.type === 'MemberExpression') { + if (assignee.property.type === 'Identifier' && assignee.property.name === '$') { + // a dollar was used so we assume this assignment is fine + return; + } + assignee = assignee.object; + } + if (assignee.type !== 'Identifier') { + return; + } + + // look for a scope that defines the variable + const variable = ASTUtils.findVariable(context.sourceCode.getScope(assignee), assignee); + // defs is an array because there may be multiple definitions with `var` + const def = variable?.defs[0]; + + // check if variable is global or was defined outside of current function by checking ranges + // NOTE: if the variable is an outer function parameter, then the enclosingFn range will be encompassed by node range + if ( + !def || + (def && (def.node.range[0] < enclosingFn.range[0] || enclosingFn.range[1] < def.node.range[1])) + ) { + context.report({ + messageId: 'jsAssignment', + node, + data: { snippet: context.sourceCode.getText(leftNode) }, + }); + return; + } + + if (def.type === 'Parameter') { + context.report({ + messageId: 'parameterAssignment', + node, + data: { snippet: context.sourceCode.getText(leftNode) }, + }); + } +} diff --git a/packages/eslint-plugin-internal/src/rules/noMath.ts b/packages/eslint-plugin-internal/src/rules/noMath.ts new file mode 100644 index 0000000000..3a70cb3dda --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noMath.ts @@ -0,0 +1,57 @@ +import { createRule } from '../ruleCreator.ts'; +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; +import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; +import { ASTUtils, type TSESTree } from '@typescript-eslint/utils'; + +export const noMath = createRule({ + name: 'no-math', + meta: { + type: 'suggestion', + docs: { + description: `Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions`, + }, + messages: { + unexpected: + "Using Math methods, such as '{{snippet}}', may not work as expected. Use 'std' instead", + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + CallExpression(node) { + if (!directives.getEnclosingTypegpuFunction()) { + return; + } + + if ( + node.callee.type === 'MemberExpression' && + node.callee.object.type === 'Identifier' && + node.callee.object.name === 'Math' && + isGlobalIdentifier(context, node.callee.object) + ) { + context.report({ + node, + messageId: 'unexpected', + data: { snippet: context.sourceCode.getText(node) }, + }); + } + }, + }; + }), +}); + +function isGlobalIdentifier( + context: Readonly>, + node: TSESTree.Identifier, +) { + const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), node); + if (!variable) { + throw new Error(`Couldn't find variable ${node.name}.`); + } + return variable.defs.length === 0; +} diff --git a/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts b/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts new file mode 100644 index 0000000000..c889ca1b2d --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts @@ -0,0 +1,41 @@ +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; +import { createRule } from '../ruleCreator.ts'; + +export const noUninitializedVariables = createRule({ + name: 'no-uninitialized-variables', + meta: { + type: 'problem', + docs: { + description: `Disallow variable declarations without initializers inside 'use gpu' functions`, + }, + messages: { + uninitializedVariable: "'{{snippet}}' must have an initial value", + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + VariableDeclarator(node) { + if (!directives.getEnclosingTypegpuFunction()) { + 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-internal/src/rules/noUnwrappedObjects.ts b/packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts new file mode 100644 index 0000000000..99f6b4d180 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts @@ -0,0 +1,49 @@ +import { enhanceRule } from '../enhanceRule.ts'; +import { directiveTracking } from '../enhancers/directiveTracking.ts'; +import { getNonTransparentParent } from '../nodeHelpers.ts'; +import { createRule } from '../ruleCreator.ts'; + +export const noUnwrappedObjects = createRule({ + name: 'no-unwrapped-objects', + meta: { + type: 'problem', + docs: { + description: `Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns)`, + }, + messages: { + unexpected: '{{snippet}} must be wrapped in a schema call', + }, + schema: [], + }, + defaultOptions: [], + + create: enhanceRule({ directives: directiveTracking }, (context, state) => { + const { directives } = state; + + return { + ObjectExpression(node) { + if (!directives.getEnclosingTypegpuFunction()) { + return; + } + let parent = getNonTransparentParent(node); + if (parent?.type === 'Property') { + // a part of a bigger struct + return; + } + if (parent?.type === 'CallExpression') { + // wrapped in a schema call + return; + } + if (parent?.type === 'ReturnStatement') { + // likely inferred (shelled fn or shell-less entry) so we cannot report + return; + } + context.report({ + node, + messageId: 'unexpected', + data: { snippet: context.sourceCode.getText(node) }, + }); + }, + }; + }), +}); diff --git a/packages/eslint-plugin-internal/tests/ruleNames.test.ts b/packages/eslint-plugin-internal/tests/ruleNames.test.ts new file mode 100644 index 0000000000..d0c9f968cc --- /dev/null +++ b/packages/eslint-plugin-internal/tests/ruleNames.test.ts @@ -0,0 +1,8 @@ +import { expect, it } from 'vitest'; +import { rules } from '../src/configs.ts'; + +it('uses the same names for rules and exports', () => { + for (const key in rules) { + expect(rules[key as keyof typeof rules].name).toBe(key); + } +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts b/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts new file mode 100644 index 0000000000..509b8e1f99 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts @@ -0,0 +1,45 @@ +import { describe } from 'vitest'; +import { noIntegerDivision } from '../../src/rules/noIntegerDivision.ts'; +import { ruleTester } from '../utils/ruleTester.ts'; + +describe('noIntegerDivision', () => { + ruleTester.run('noIntegerDivision', noIntegerDivision, { + valid: ['1 / 2', 'd.u32(d.u32(1) / d.u32(2))'], + invalid: [ + { + code: 'd.u32(1) / 2', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / 2' } }], + }, + { + code: '1 / d.u32(2)', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: '1 / d.u32(2)' } }], + }, + { + code: 'd.u32(1) / d.u32(2)', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.u32(2)' } }], + }, + { + code: 'd.i32(1) / d.i32(2)', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.i32(1) / d.i32(2)' } }], + }, + { + code: 'd.u32(1) / d.i32(2)', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.i32(2)' } }], + }, + { + code: 'u32(1) / u32(2)', + errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'u32(1) / u32(2)' } }], + }, + { + code: 'd.u32(1) / d.u32(2) / d.u32(3)', + errors: [ + { + messageId: 'suspiciousDivision', + data: { snippet: 'd.u32(1) / d.u32(2) / d.u32(3)' }, + }, + { messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.u32(2)' } }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts b/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts new file mode 100644 index 0000000000..b06d16a33a --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts @@ -0,0 +1,210 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noInvalidAssignment } from '../../src/rules/noInvalidAssignment.ts'; + +describe('noInvalidAssignment', () => { + ruleTester.run('noInvalidAssignment', noInvalidAssignment, { + valid: [ + // not inside 'use gpu' function + 'const fn = (a) => { a = {}; }', + 'const fn = (a) => { a.prop = 1; }', + "const fn = (a) => { a['prop'] = 1; }", + 'const fn = (a) => { a[0] = 1; }', + + // not using parameter + "const fn = (a) => { 'use gpu'; let b = 0; b = 1; }", + "const fn = (a) => { 'use gpu'; { let a = 1; a = 2; } }", + + // correctly accessed + "const fn = (a) => { 'use gpu'; a.$ = 1 }", + "const fn = (a) => { 'use gpu'; a.$++; }", + "const fn = (a) => { 'use gpu'; a.$ += 1; }", + ], + invalid: [ + { + code: "const fn = (a) => { 'use gpu'; a = 1; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "let a; const fn = (a) => { 'use gpu'; a = 1; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a) => { 'use gpu'; a.prop = 1; }", + errors: [ + { + messageId: 'parameterAssignment', + data: { snippet: 'a.prop' }, + }, + ], + }, + { + code: "const fn = (a) => { 'use gpu'; a['prop'] = 1; }", + errors: [ + { + messageId: 'parameterAssignment', + data: { snippet: "a['prop']" }, + }, + ], + }, + { + code: "const fn = (a) => { 'use gpu'; a[0] = 1; }", + errors: [ + { + messageId: 'parameterAssignment', + data: { snippet: 'a[0]' }, + }, + ], + }, + { + code: "const fn = (a) => { 'use gpu'; a++; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a) => { 'use gpu'; --a; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = ({a}) => { 'use gpu'; a = 1; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a) => { 'use gpu'; a += 1; }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a) => { 'use gpu'; a.prop1.prop2 = 1; }", + errors: [ + { + messageId: 'parameterAssignment', + data: { snippet: 'a.prop1.prop2' }, + }, + ], + }, + { + code: "const fn = (a) => { 'use gpu'; if (true) { a = 1; } }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a) => { 'use gpu'; a = 1; { let a; } }", + errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], + }, + { + code: "const fn = (a, b) => { 'use gpu'; a = 1; b = 2; }", + errors: [ + { messageId: 'parameterAssignment', data: { snippet: 'a' } }, + { messageId: 'parameterAssignment', data: { snippet: 'b' } }, + ], + }, + { + code: "const fn = (a) => { 'use gpu'; a.$prop = 1; }", + errors: [ + { + messageId: 'parameterAssignment', + data: { snippet: 'a.$prop' }, + }, + ], + }, + ], + }); + + ruleTester.run('invalidAssignment', noInvalidAssignment, { + valid: [ + // not inside 'use gpu' function + 'let a; const fn = () => { a = 1 }', + 'const outer = (a) => { const fn = () => { a = 1 } }', + 'const vars = []; const fn = () => { vars[0] = 1 }', + + // correctly accessed + "const buffer = {}; const fn = () => { 'use gpu'; buffer.$ = 1 }", + "const outer = (buffer) => { const fn = () => { 'use gpu'; buffer.$ = 1 } }", + "const buffers = []; const fn = () => { 'use gpu'; buffers[0].$ = 1 }", + ], + invalid: [ + { + code: "let a; const fn = () => { 'use gpu'; a = 1 }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "var a; const fn = () => { 'use gpu'; a = 1 }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "const outer = (a) => { const fn = () => { 'use gpu'; a = 1 } }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "const a = {}; const fn = () => { 'use gpu'; a.prop = 1; }", + errors: [ + { + messageId: 'jsAssignment', + data: { snippet: 'a.prop' }, + }, + ], + }, + { + code: "const a = {}; const fn = () => { 'use gpu'; a['prop'] = 1; }", + errors: [ + { + messageId: 'jsAssignment', + data: { snippet: "a['prop']" }, + }, + ], + }, + { + code: "const vars = []; const fn = () => { 'use gpu'; vars[0] = 1 }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'vars[0]' } }], + }, + { + code: "const fn = () => { 'use gpu'; a += 1; }; let a;", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "let a; const fn = () => { 'use gpu'; a++; }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "let a; const fn = () => { 'use gpu'; a += 1; }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "const a = {}; const fn = () => { 'use gpu'; a.prop1.prop2 = 1; }", + errors: [ + { + messageId: 'jsAssignment', + data: { snippet: 'a.prop1.prop2' }, + }, + ], + }, + { + code: "let a; const fn = () => { 'use gpu'; if (true) { a = 1; } }", + errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], + }, + { + code: "let a, b; const fn = () => { 'use gpu'; a = 1; b = 2; }", + errors: [ + { messageId: 'jsAssignment', data: { snippet: 'a' } }, + { messageId: 'jsAssignment', data: { snippet: 'b' } }, + ], + }, + { + code: "const a = {}; const fn = () => { 'use gpu'; a.$prop = 1; }", + errors: [ + { + messageId: 'jsAssignment', + data: { snippet: 'a.$prop' }, + }, + ], + }, + { + code: "const fn = () => { 'use gpu'; globalThis.prop = 1 }", + errors: [ + { + messageId: 'jsAssignment', + data: { snippet: 'globalThis.prop' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noMath.test.ts b/packages/eslint-plugin-internal/tests/rules/noMath.test.ts new file mode 100644 index 0000000000..3ddd2708f8 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noMath.test.ts @@ -0,0 +1,26 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noMath } from '../../src/rules/noMath.ts'; + +describe('noMath', () => { + ruleTester.run('noMath', noMath, { + valid: [ + 'const result = Math.sin(1);', + 'const t = std.sin(Math.PI)', + "const fn = () => { 'use gpu'; const vec = std.sin(Math.PI); }", + "const Math = { sin: std.sin }; const fn = () => { 'use gpu'; const vec = Math.sin(0); }", + "import Math from 'utils'; const fn = () => { 'use gpu'; const vec = Math.sin(0); }", + ], + invalid: [ + { + code: "const fn = () => { 'use gpu'; const vec = Math.sin(0); }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: 'Math.sin(0)' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts b/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts new file mode 100644 index 0000000000..96dc6ca5c3 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts @@ -0,0 +1,44 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noUninitializedVariables } from '../../src/rules/noUninitializedVariables.ts'; + +describe('noUninitializedVariables', () => { + ruleTester.run('noUninitializedVariables', noUninitializedVariables, { + 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/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts b/packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts new file mode 100644 index 0000000000..1ca1e919a5 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts @@ -0,0 +1,79 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noUnwrappedObjects } from '../../src/rules/noUnwrappedObjects.ts'; + +describe('noUnwrappedObjects', () => { + ruleTester.run('noUnwrappedObjects', noUnwrappedObjects, { + valid: [ + // correctly wrapped + "function func() { 'use gpu'; const wrapped = Schema({ a: 1 }); }", + "const func = function() { 'use gpu'; const wrapped = Schema({ a: 1 }); }", + "() => { 'use gpu'; const wrapped = Schema({ a: 1 }); }", + + // not inside 'use gpu' function + 'const pojo = { a: 1 };', + 'function func() { const unwrapped = { a: 1 }; }', + 'const func = function () { const unwrapped = { a: 1 }; }', + '() => { const unwrapped = { a: 1 }; }', + 'function func() { return { a: 1 }; }', + 'const func = function () { return { a: 1 }; }', + '() => { return { a: 1 }; }', + + // return from 'use gpu' function + "function func() { 'use gpu'; return { a: 1 }; }", + "const func = function() { 'use gpu'; return { a: 1 }; }", + "() => { 'use gpu'; return { a: 1 }; }", + "() => { 'use gpu'; return { a: { b: 1 } }; }", + "() => { 'use gpu'; return { a: 1 } as typeof Struct; }", + "() => { 'use gpu'; return { a: 1 } satisfies Struct; }", + "() => { 'use gpu'; return ({ a: 1 }); }", + ], + invalid: [ + { + code: "function func() { 'use gpu'; const unwrapped = { a: 1 }; }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: '{ a: 1 }' }, + }, + ], + }, + { + code: "const func = function() { 'use gpu'; const unwrapped = { a: 1 }; }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: '{ a: 1 }' }, + }, + ], + }, + { + code: "() => { 'use gpu'; const unwrapped = { a: 1 }; }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: '{ a: 1 }' }, + }, + ], + }, + { + code: "function func() { 'unknown directive'; 'use gpu'; const unwrapped = { a: 1 }; }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: '{ a: 1 }' }, + }, + ], + }, + { + code: "() => { 'use gpu'; const unwrapped = { a: { b: 1 } }; }", + errors: [ + { + messageId: 'unexpected', + data: { snippet: '{ a: { b: 1 } }' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/utils/ruleTester.ts b/packages/eslint-plugin-internal/tests/utils/ruleTester.ts new file mode 100644 index 0000000000..3efee657d3 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/utils/ruleTester.ts @@ -0,0 +1,10 @@ +import { RuleTester } from '@typescript-eslint/rule-tester'; +import { afterAll, describe, it } from 'vitest'; + +// RuleTester relies on global hooks for tests. +// Vitest doesn't make the hooks available globally, so we need to bind them. +RuleTester.describe = describe; +RuleTester.it = it; +RuleTester.afterAll = afterAll; + +export const ruleTester = new RuleTester(); diff --git a/packages/eslint-plugin-internal/tsconfig.json b/packages/eslint-plugin-internal/tsconfig.json new file mode 100644 index 0000000000..3a544d3dd8 --- /dev/null +++ b/packages/eslint-plugin-internal/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "types": ["node"] + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/eslint-plugin-internal/tsdown.config.ts b/packages/eslint-plugin-internal/tsdown.config.ts new file mode 100644 index 0000000000..3ec1155cc3 --- /dev/null +++ b/packages/eslint-plugin-internal/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, +}); diff --git a/packages/eslint-plugin-internal/vitest.config.ts b/packages/eslint-plugin-internal/vitest.config.ts new file mode 100644 index 0000000000..b0e95a5a89 --- /dev/null +++ b/packages/eslint-plugin-internal/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.ts'], + environment: 'node', + }, +}); From 3c52c108ae21aec28d8902c4c7edcd7676c13957 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 10:57:53 +0200 Subject: [PATCH 02/12] Strip the plugin of unused code, enable plugin in config --- oxlint.config.ts | 3 +- package.json | 1 + .../docs/rules/no-integer-division.md | 34 --- .../docs/rules/no-invalid-assignment.md | 54 ----- .../docs/rules/no-math.md | 31 --- .../docs/rules/no-uninitialized-variables.md | 31 --- .../docs/rules/no-unwrapped-objects.md | 38 ---- packages/eslint-plugin-internal/package.json | 37 +-- .../eslint-plugin-internal/src/configs.ts | 32 --- .../eslint-plugin-internal/src/enhanceRule.ts | 80 ------- .../src/enhancers/directiveTracking.ts | 83 ------- packages/eslint-plugin-internal/src/index.ts | 28 +-- .../eslint-plugin-internal/src/nodeHelpers.ts | 20 -- .../eslint-plugin-internal/src/ruleCreator.ts | 1 - .../src/rules/noIntegerDivision.ts | 61 ----- .../src/rules/noInvalidAssignment.ts | 90 -------- .../src/rules/noMath.ts | 42 +--- .../src/rules/noUninitializedVariables.ts | 41 ---- .../src/rules/noUnwrappedObjects.ts | 49 ---- .../tests/ruleNames.test.ts | 8 - .../tests/rules/noIntegerDivision.test.ts | 45 ---- .../tests/rules/noInvalidAssignment.test.ts | 210 ------------------ .../tests/rules/noMath.test.ts | 26 --- .../rules/noUninitializedVariables.test.ts | 44 ---- .../tests/rules/noUnwrappedObjects.test.ts | 79 ------- pnpm-lock.yaml | 31 +++ 26 files changed, 52 insertions(+), 1147 deletions(-) delete mode 100644 packages/eslint-plugin-internal/docs/rules/no-integer-division.md delete mode 100644 packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md delete mode 100644 packages/eslint-plugin-internal/docs/rules/no-math.md delete mode 100644 packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md delete mode 100644 packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md delete mode 100644 packages/eslint-plugin-internal/src/configs.ts delete mode 100644 packages/eslint-plugin-internal/src/enhanceRule.ts delete mode 100644 packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts delete mode 100644 packages/eslint-plugin-internal/src/nodeHelpers.ts delete mode 100644 packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts delete mode 100644 packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts delete mode 100644 packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts delete mode 100644 packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts delete mode 100644 packages/eslint-plugin-internal/tests/ruleNames.test.ts delete mode 100644 packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts delete mode 100644 packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts delete mode 100644 packages/eslint-plugin-internal/tests/rules/noMath.test.ts delete mode 100644 packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts delete mode 100644 packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts diff --git a/oxlint.config.ts b/oxlint.config.ts index 37983778e0..07521e72e9 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -7,7 +7,7 @@ const typegpuRules = typegpuPreset && 'rules' in typegpuPreset ? typegpuPreset.r export default defineConfig({ plugins: ['eslint', 'typescript', 'import', 'unicorn', 'oxc'], - jsPlugins: ['eslint-plugin-typegpu', 'eslint-plugin-eslint-plugin'], + jsPlugins: ['eslint-plugin-typegpu', 'eslint-plugin-eslint-plugin', 'eslint-plugin-internal'], categories: { correctness: 'warn', suspicious: 'warn', @@ -24,6 +24,7 @@ export default defineConfig({ 'eslint-plugin-import/no-named-as-default': 'off', 'eslint-plugin-import/no-named-as-default-member': 'off', 'eslint-plugin-import/extensions': ['error', 'always', { ignorePackages: true }], + 'eslint-plugin-internal/no-math': 'error', }, ignorePatterns: ['**/*.astro', '**/*.mjs'], overrides: [ diff --git a/package.json b/package.json index d9c199c59d..7711ca82a6 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@webgpu/types": "catalog:types", "dpdm": "^3.14.0", "eslint-plugin-eslint-plugin": "^7.3.2", + "eslint-plugin-internal": "workspace:*", "eslint-plugin-typegpu": "workspace:*", "jiti": "catalog:build", "oxfmt": "^0.35.0", diff --git a/packages/eslint-plugin-internal/docs/rules/no-integer-division.md b/packages/eslint-plugin-internal/docs/rules/no-integer-division.md deleted file mode 100644 index 4c934c6282..0000000000 --- a/packages/eslint-plugin-internal/docs/rules/no-integer-division.md +++ /dev/null @@ -1,34 +0,0 @@ -# typegpu/no-integer-division - -📝 Disallow division incorporating numbers wrapped in 'u32' and 'i32'. - -⚠️ This rule _warns_ in the ⭐ `recommended` config. - - - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -const a = d.u32(1) / d.u32(2); -``` -```ts -const a = 1 / d.u32(2); -``` -```ts -const a = 1 / d.i32(2); -``` - -Examples of **correct** code for this rule: - -```ts -const a = 1 / 2; -``` -```ts -const a = d.u32(d.u32(1) / d.u32(2)); -``` - -Note that this rule is not type aware. -Extracting the dividend and the divisor to variables will silence the rule, -but it will not make the code behave differently. \ No newline at end of file diff --git a/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md b/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md deleted file mode 100644 index 5fb48326f4..0000000000 --- a/packages/eslint-plugin-internal/docs/rules/no-invalid-assignment.md +++ /dev/null @@ -1,54 +0,0 @@ -# typegpu/no-invalid-assignment - -📝 Disallow assignments that will generate invalid WGSL. - -🚨 This rule is enabled in the ⭐ `recommended` config. - - - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -const fn = (a) => { - 'use gpu'; - a = 1; -} -``` -```ts -const fn = (a) => { - 'use gpu'; - a.prop++; -} -``` -```ts -let a; -const fn = () => { - 'use gpu'; - a = 1; -} -``` - -Examples of **correct** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - const ref = d.ref(0); - other(ref); -}; - -const other = (ref: d.ref) => { - 'use gpu'; - ref.$ = 1; -}; -``` -```ts -const privateVar = tgpu.privateVar(d.u32); -const fn = () => { - 'use gpu'; - privateVar.$ = 1; -} -``` - diff --git a/packages/eslint-plugin-internal/docs/rules/no-math.md b/packages/eslint-plugin-internal/docs/rules/no-math.md deleted file mode 100644 index c136e5d04c..0000000000 --- a/packages/eslint-plugin-internal/docs/rules/no-math.md +++ /dev/null @@ -1,31 +0,0 @@ -# typegpu/no-math - -📝 Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions. - -⚠️ This rule _warns_ in the ⭐ `recommended` config. - - - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - const vec = Math.sin(0); -} -``` - -Examples of **correct** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - const a = std.sin(Math.PI); -} -``` -```ts -// outside 'use gpu' -const a = Math.sin(1); -``` \ No newline at end of file diff --git a/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md b/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md deleted file mode 100644 index f272ab93b9..0000000000 --- a/packages/eslint-plugin-internal/docs/rules/no-uninitialized-variables.md +++ /dev/null @@ -1,31 +0,0 @@ -# typegpu/no-uninitialized-variables - -📝 Disallow variable declarations without initializers inside 'use gpu' functions. - -🚨 This rule is enabled in the ⭐ `recommended` config. - - - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - let a; -} -``` - -Examples of **correct** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - let vec = d.vec3f(); -} -``` -```ts -// outside 'use gpu' -let a; -``` diff --git a/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md b/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md deleted file mode 100644 index bcdeb7e20c..0000000000 --- a/packages/eslint-plugin-internal/docs/rules/no-unwrapped-objects.md +++ /dev/null @@ -1,38 +0,0 @@ -# typegpu/no-unwrapped-objects - -📝 Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns). - -🚨 This rule is enabled in the ⭐ `recommended` config. - - - -## Rule details - -Examples of **incorrect** code for this rule: - -```ts -const fn = () => { - 'use gpu'; - const unwrapped = { a: 1 }; -} -``` - -Examples of **correct** code for this rule: - -```ts -const pojo = { a: 1 }; -``` -```ts -const fn = () => { - 'use gpu'; - return { a: 1 }; -} -``` -```ts -const Schema = d.struct({ a: d.u32 }); - -const fn = () => { - 'use gpu'; - const wrapped = Schema({ a: 1 }); -} -``` \ No newline at end of file diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 003192a77f..0ed80ab20d 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,23 +1,14 @@ { - "name": "eslint-plugin-typegpu", - "version": "0.10.0-alpha.2", - "description": "TypeGPU specific linting rules for ESLint", - "keywords": [ - "eslint", - "eslint-plugin", - "eslintplugin", - "typegpu", - "use gpu" - ], + "name": "eslint-plugin-internal", + "version": "0.10.0", + "private": true, + "description": "Ruleset for working on TypeGPU monorepo", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/software-mansion/TypeGPU.git#main", - "directory": "packages/eslint-plugin" + "directory": "packages/eslint-plugin-internal" }, - "files": [ - "dist" - ], "type": "module", "sideEffects": false, "main": "./src/index.ts", @@ -27,24 +18,8 @@ "import": "./src/index.ts" } }, - "publishConfig": { - "exports": { - ".": { - "types": "./dist/index.d.mts", - "import": "./dist/index.mjs" - } - }, - "main": "./dist/index.mjs", - "types": "./dist/index.d.mts" - }, "scripts": { - "build": "tsdown", - "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", - "docs:init": "eslint-doc-generator --init-rule-docs", - "docs:check": "eslint-doc-generator --check", - "docs:update": "eslint-doc-generator", - "test": "vitest", - "prepublishOnly": "pnpm run docs:check && pnpm run test:types && vitest run && pnpm run build" + "test:types": "pnpm tsc --p ./tsconfig.json --noEmit" }, "dependencies": { "@typescript-eslint/utils": "^8.57.2" diff --git a/packages/eslint-plugin-internal/src/configs.ts b/packages/eslint-plugin-internal/src/configs.ts deleted file mode 100644 index df130b1dcd..0000000000 --- a/packages/eslint-plugin-internal/src/configs.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { TSESLint } from '@typescript-eslint/utils'; -import { noIntegerDivision } from './rules/noIntegerDivision.ts'; -import { noUnwrappedObjects } from './rules/noUnwrappedObjects.ts'; -import { noMath } from './rules/noMath.ts'; -import { noUninitializedVariables } from './rules/noUninitializedVariables.ts'; -import { noInvalidAssignment } from './rules/noInvalidAssignment.ts'; - -export const rules = { - 'no-integer-division': noIntegerDivision, - 'no-unwrapped-objects': noUnwrappedObjects, - 'no-uninitialized-variables': noUninitializedVariables, - 'no-math': noMath, - 'no-invalid-assignment': noInvalidAssignment, -} as const; - -type Rules = Record<`typegpu/${keyof typeof rules}`, TSESLint.FlatConfig.RuleEntry>; - -export const recommendedRules: Rules = { - 'typegpu/no-integer-division': 'warn', - 'typegpu/no-unwrapped-objects': 'error', - 'typegpu/no-uninitialized-variables': 'error', - 'typegpu/no-math': 'warn', - 'typegpu/no-invalid-assignment': 'error', -}; - -export const allRules: Rules = { - 'typegpu/no-integer-division': 'error', - 'typegpu/no-unwrapped-objects': 'error', - 'typegpu/no-uninitialized-variables': 'error', - 'typegpu/no-math': 'error', - 'typegpu/no-invalid-assignment': 'error', -}; diff --git a/packages/eslint-plugin-internal/src/enhanceRule.ts b/packages/eslint-plugin-internal/src/enhanceRule.ts deleted file mode 100644 index 5d0a366b68..0000000000 --- a/packages/eslint-plugin-internal/src/enhanceRule.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { RuleContext, RuleListener } from '@typescript-eslint/utils/ts-eslint'; - -export type RuleEnhancer = (context: RuleContext) => { - visitors: RuleListener; - state: TState; -}; - -type State>> = { - [K in keyof TMap]: TMap[K] extends RuleEnhancer ? S : never; -}; - -/** - * Allows enhancing rule code with additional context provided by RuleEnhancers (reusable node visitors collecting data). - * @param enhancers a record of RuleEnhancers - * @param rule a visitor with an additional `state` argument that allows access to the enhancers' data - * @returns a resulting `(context: Context) => RuleListener` function - * - * @example - * // inside of `createRule` - * create: enhanceRule({ metadata: metadataTrackingEnhancer }, (context, state) => { - * const { metadata } = state; - * - * return { - * ObjectExpression(node) { - * if (metadata.shouldReport()) { - * context.report({ node, messageId: 'error' }); - * } - * }, - * }; - */ -export function enhanceRule< - TMap extends Record>, - Context extends RuleContext, ->(enhancers: TMap, rule: (context: Context, state: State) => RuleListener) { - return (context: Context) => { - const enhancerVisitors: RuleListener[] = []; - const combinedState: Record = {}; - - for (const [key, enhancer] of Object.entries(enhancers)) { - const initializedEnhancer = enhancer(context); - enhancerVisitors.push(initializedEnhancer.visitors); - combinedState[key] = initializedEnhancer.state; - } - - const initializedRule = rule(context, combinedState as State); - - return mergeVisitors([...enhancerVisitors, initializedRule]); - }; -} - -/** - * Merges all passed visitors into one visitor. - * Retains visitor order: - * - on node enter, visitors are called in `visitorsList` order, - * - on node exit, visitors are called in reversed order. - */ -function mergeVisitors(visitors: RuleListener[]): RuleListener { - const merged: RuleListener = {}; - - const allKeys = new Set(visitors.flatMap((v) => Object.keys(v))); - - for (const key of allKeys) { - const listeners = visitors.map((v) => v[key]).filter((fn) => fn !== undefined); - - if (listeners.length === 0) { - continue; - } - - // Reverse order if node is an exit node - if (key.endsWith(':exit')) { - listeners.reverse(); - } - - merged[key] = (...args: unknown[]) => { - listeners.forEach((fn) => (fn as (...args: unknown[]) => void)(...args)); - }; - } - - return merged; -} diff --git a/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts b/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts deleted file mode 100644 index b490760afb..0000000000 --- a/packages/eslint-plugin-internal/src/enhancers/directiveTracking.ts +++ /dev/null @@ -1,83 +0,0 @@ -import type { TSESTree } from '@typescript-eslint/utils'; -import type { RuleListener } from '@typescript-eslint/utils/ts-eslint'; -import type { RuleEnhancer } from '../enhanceRule.ts'; - -export type FunctionNode = - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression; - -export type DirectiveData = { - getEnclosingTypegpuFunction: () => FunctionNode | undefined; -}; - -/** - * A RuleEnhancer that tracks whether the current node is inside a 'use gpu' function. - * - * @privateRemarks - * Should the need arise, the API could be updated to expose: - * - a list of directives of the current function, - * - directives of other visited functions, - * - top level directives. - */ -export const directiveTracking: RuleEnhancer = () => { - const stack: { node: FunctionNode; directives: string[] }[] = []; - - const visitors: RuleListener = { - FunctionDeclaration(node) { - stack.push({ node, directives: getDirectives(node) }); - }, - FunctionExpression(node) { - stack.push({ node, directives: getDirectives(node) }); - }, - ArrowFunctionExpression(node) { - stack.push({ node, directives: getDirectives(node) }); - }, - - 'FunctionDeclaration:exit'() { - stack.pop(); - }, - 'FunctionExpression:exit'() { - stack.pop(); - }, - 'ArrowFunctionExpression:exit'() { - stack.pop(); - }, - }; - - return { - visitors, - state: { - getEnclosingTypegpuFunction: () => { - const current = stack.at(-1); - if (current && current.directives.includes('use gpu')) { - return current.node; - } - return undefined; - }, - }, - }; -}; - -function getDirectives( - node: - | TSESTree.FunctionDeclaration - | TSESTree.FunctionExpression - | TSESTree.ArrowFunctionExpression, -): string[] { - const body = node.body; - if (body.type !== 'BlockStatement') { - return []; - } - - const directives: string[] = []; - for (const statement of body.body) { - if (statement.type === 'ExpressionStatement' && statement.directive) { - directives.push(statement.directive); - } else { - break; - } - } - - return directives; -} diff --git a/packages/eslint-plugin-internal/src/index.ts b/packages/eslint-plugin-internal/src/index.ts index 08763811d3..5e4ceb23ff 100644 --- a/packages/eslint-plugin-internal/src/index.ts +++ b/packages/eslint-plugin-internal/src/index.ts @@ -1,33 +1,15 @@ import pkg from '../package.json' with { type: 'json' }; import type { TSESLint } from '@typescript-eslint/utils'; -import { allRules, recommendedRules, rules } from './configs.ts'; +import { noMath } from './rules/noMath.ts'; -const pluginBase: TSESLint.FlatConfig.Plugin = { +const plugin = { meta: { name: pkg.name, version: pkg.version, }, - rules, -}; - -const recommended: TSESLint.FlatConfig.Config = { - name: 'typegpu/recommended', - plugins: { typegpu: pluginBase }, - rules: recommendedRules, -}; - -const all: TSESLint.FlatConfig.Config = { - name: 'typegpu/all', - plugins: { typegpu: pluginBase }, - rules: allRules, -}; - -const plugin: TSESLint.FlatConfig.Plugin = { - ...pluginBase, - configs: { - recommended, - all, + rules: { + 'no-math': noMath, }, -}; +} satisfies TSESLint.FlatConfig.Plugin; export default plugin; diff --git a/packages/eslint-plugin-internal/src/nodeHelpers.ts b/packages/eslint-plugin-internal/src/nodeHelpers.ts deleted file mode 100644 index 4122427d17..0000000000 --- a/packages/eslint-plugin-internal/src/nodeHelpers.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { TSESTree } from '@typescript-eslint/utils'; - -const transparentNodes = [ - 'TSAsExpression', - 'TSSatisfiesExpression', - 'TSTypeAssertion', - 'TSNonNullExpression', -]; - -export function isTransparent(node: TSESTree.Node): boolean { - return transparentNodes.includes(node.type); -} - -export function getNonTransparentParent(node: TSESTree.Node) { - let parent = node.parent; - while (parent && isTransparent(parent)) { - parent = parent.parent; - } - return parent; -} diff --git a/packages/eslint-plugin-internal/src/ruleCreator.ts b/packages/eslint-plugin-internal/src/ruleCreator.ts index 6d07477ffc..d4fa2d5089 100644 --- a/packages/eslint-plugin-internal/src/ruleCreator.ts +++ b/packages/eslint-plugin-internal/src/ruleCreator.ts @@ -1,6 +1,5 @@ import { ESLintUtils } from '@typescript-eslint/utils'; export const createRule = ESLintUtils.RuleCreator( - // TODO: docs for lint rules () => `https://docs.swmansion.com/TypeGPU/getting-started/`, ); diff --git a/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts b/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts deleted file mode 100644 index 99664f29c7..0000000000 --- a/packages/eslint-plugin-internal/src/rules/noIntegerDivision.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { TSESTree } from '@typescript-eslint/utils'; -import { createRule } from '../ruleCreator.ts'; - -// TODO: detect `std.div(d.u32(1), d.u32(2))` -export const noIntegerDivision = createRule({ - name: 'no-integer-division', - meta: { - type: 'suggestion', - docs: { description: `Disallow division incorporating numbers wrapped in 'u32' and 'i32'` }, - messages: { - suspiciousDivision: - "'{{snippet}}' might result in floating point values. To perform integer division, wrap the result in 'd.u32' or 'd.i32' instead", - }, - schema: [], - }, - defaultOptions: [], - - create(context) { - return { - BinaryExpression(node) { - if (node.operator !== '/') { - return; - } - - if (node.parent?.type === 'CallExpression' && isIntCast(node.parent)) { - return; - } - - if (isIntCast(node.left) || isIntCast(node.right)) { - context.report({ - node, - messageId: 'suspiciousDivision', - data: { snippet: context.sourceCode.getText(node) }, - }); - } - }, - }; - }, -}); - -/** - * Checks if a node is a call expression to an integer cast function (i32 or u32). - * - * @example - * // for simplicity, using code snippets instead of ASTs - * isIntCasts('d.u32()'); // true - * isIntCasts('i32()'); // true - * isIntCasts('f32()'); // false - */ -function isIntCast(node: TSESTree.Expression): boolean { - if (node.type !== 'CallExpression') { - return false; - } - - let callee: TSESTree.Node = node.callee; - while (callee.type === 'MemberExpression') { - callee = callee.property; - } - - return callee.type === 'Identifier' && ['i32', 'u32'].includes(callee.name); -} diff --git a/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts b/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts deleted file mode 100644 index 80d4cbf278..0000000000 --- a/packages/eslint-plugin-internal/src/rules/noInvalidAssignment.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { ASTUtils, type TSESTree } from '@typescript-eslint/utils'; -import { createRule } from '../ruleCreator.ts'; -import { enhanceRule } from '../enhanceRule.ts'; -import { directiveTracking } from '../enhancers/directiveTracking.ts'; -import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; - -export const noInvalidAssignment = createRule({ - name: 'no-invalid-assignment', - meta: { - type: 'problem', - docs: { - description: `Disallow assignments that will generate invalid WGSL`, - }, - messages: { - parameterAssignment: - "Cannot assign to '{{snippet}}' since WGSL parameters are immutable. If you're using d.ref, please either use '.$' or disable this rule", - jsAssignment: - "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", - }, - schema: [], - }, - defaultOptions: [], - - create: enhanceRule({ directives: directiveTracking }, (context, state) => { - const { directives } = state; - - return { - UpdateExpression(node) { - const enclosingFn = directives.getEnclosingTypegpuFunction(); - validateAssignment(context, node, enclosingFn, node.argument); - }, - - AssignmentExpression(node) { - const enclosingFn = directives.getEnclosingTypegpuFunction(); - validateAssignment(context, node, enclosingFn, node.left); - }, - }; - }), -}); - -function validateAssignment( - context: Readonly>, - node: TSESTree.Node, - enclosingFn: TSESTree.Node | undefined, - leftNode: TSESTree.Node, -) { - if (!enclosingFn) { - return; - } - - // follow the member expression chain - let assignee = leftNode; - while (assignee.type === 'MemberExpression') { - if (assignee.property.type === 'Identifier' && assignee.property.name === '$') { - // a dollar was used so we assume this assignment is fine - return; - } - assignee = assignee.object; - } - if (assignee.type !== 'Identifier') { - return; - } - - // look for a scope that defines the variable - const variable = ASTUtils.findVariable(context.sourceCode.getScope(assignee), assignee); - // defs is an array because there may be multiple definitions with `var` - const def = variable?.defs[0]; - - // check if variable is global or was defined outside of current function by checking ranges - // NOTE: if the variable is an outer function parameter, then the enclosingFn range will be encompassed by node range - if ( - !def || - (def && (def.node.range[0] < enclosingFn.range[0] || enclosingFn.range[1] < def.node.range[1])) - ) { - context.report({ - messageId: 'jsAssignment', - node, - data: { snippet: context.sourceCode.getText(leftNode) }, - }); - return; - } - - if (def.type === 'Parameter') { - context.report({ - messageId: 'parameterAssignment', - node, - data: { snippet: context.sourceCode.getText(leftNode) }, - }); - } -} diff --git a/packages/eslint-plugin-internal/src/rules/noMath.ts b/packages/eslint-plugin-internal/src/rules/noMath.ts index 3a70cb3dda..1a79bfb9dc 100644 --- a/packages/eslint-plugin-internal/src/rules/noMath.ts +++ b/packages/eslint-plugin-internal/src/rules/noMath.ts @@ -1,8 +1,4 @@ import { createRule } from '../ruleCreator.ts'; -import { enhanceRule } from '../enhanceRule.ts'; -import { directiveTracking } from '../enhancers/directiveTracking.ts'; -import type { RuleContext } from '@typescript-eslint/utils/ts-eslint'; -import { ASTUtils, type TSESTree } from '@typescript-eslint/utils'; export const noMath = createRule({ name: 'no-math', @@ -19,39 +15,15 @@ export const noMath = createRule({ }, defaultOptions: [], - create: enhanceRule({ directives: directiveTracking }, (context, state) => { - const { directives } = state; - + create(context) { return { CallExpression(node) { - if (!directives.getEnclosingTypegpuFunction()) { - return; - } - - if ( - node.callee.type === 'MemberExpression' && - node.callee.object.type === 'Identifier' && - node.callee.object.name === 'Math' && - isGlobalIdentifier(context, node.callee.object) - ) { - context.report({ - node, - messageId: 'unexpected', - data: { snippet: context.sourceCode.getText(node) }, - }); - } + context.report({ + node, + messageId: 'unexpected', + data: { snippet: context.sourceCode.getText(node) }, + }); }, }; - }), + }, }); - -function isGlobalIdentifier( - context: Readonly>, - node: TSESTree.Identifier, -) { - const variable = ASTUtils.findVariable(context.sourceCode.getScope(node), node); - if (!variable) { - throw new Error(`Couldn't find variable ${node.name}.`); - } - return variable.defs.length === 0; -} diff --git a/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts b/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts deleted file mode 100644 index c889ca1b2d..0000000000 --- a/packages/eslint-plugin-internal/src/rules/noUninitializedVariables.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { enhanceRule } from '../enhanceRule.ts'; -import { directiveTracking } from '../enhancers/directiveTracking.ts'; -import { createRule } from '../ruleCreator.ts'; - -export const noUninitializedVariables = createRule({ - name: 'no-uninitialized-variables', - meta: { - type: 'problem', - docs: { - description: `Disallow variable declarations without initializers inside 'use gpu' functions`, - }, - messages: { - uninitializedVariable: "'{{snippet}}' must have an initial value", - }, - schema: [], - }, - defaultOptions: [], - - create: enhanceRule({ directives: directiveTracking }, (context, state) => { - const { directives } = state; - - return { - VariableDeclarator(node) { - if (!directives.getEnclosingTypegpuFunction()) { - 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-internal/src/rules/noUnwrappedObjects.ts b/packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts deleted file mode 100644 index 99f6b4d180..0000000000 --- a/packages/eslint-plugin-internal/src/rules/noUnwrappedObjects.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { enhanceRule } from '../enhanceRule.ts'; -import { directiveTracking } from '../enhancers/directiveTracking.ts'; -import { getNonTransparentParent } from '../nodeHelpers.ts'; -import { createRule } from '../ruleCreator.ts'; - -export const noUnwrappedObjects = createRule({ - name: 'no-unwrapped-objects', - meta: { - type: 'problem', - docs: { - description: `Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns)`, - }, - messages: { - unexpected: '{{snippet}} must be wrapped in a schema call', - }, - schema: [], - }, - defaultOptions: [], - - create: enhanceRule({ directives: directiveTracking }, (context, state) => { - const { directives } = state; - - return { - ObjectExpression(node) { - if (!directives.getEnclosingTypegpuFunction()) { - return; - } - let parent = getNonTransparentParent(node); - if (parent?.type === 'Property') { - // a part of a bigger struct - return; - } - if (parent?.type === 'CallExpression') { - // wrapped in a schema call - return; - } - if (parent?.type === 'ReturnStatement') { - // likely inferred (shelled fn or shell-less entry) so we cannot report - return; - } - context.report({ - node, - messageId: 'unexpected', - data: { snippet: context.sourceCode.getText(node) }, - }); - }, - }; - }), -}); diff --git a/packages/eslint-plugin-internal/tests/ruleNames.test.ts b/packages/eslint-plugin-internal/tests/ruleNames.test.ts deleted file mode 100644 index d0c9f968cc..0000000000 --- a/packages/eslint-plugin-internal/tests/ruleNames.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { expect, it } from 'vitest'; -import { rules } from '../src/configs.ts'; - -it('uses the same names for rules and exports', () => { - for (const key in rules) { - expect(rules[key as keyof typeof rules].name).toBe(key); - } -}); diff --git a/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts b/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts deleted file mode 100644 index 509b8e1f99..0000000000 --- a/packages/eslint-plugin-internal/tests/rules/noIntegerDivision.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe } from 'vitest'; -import { noIntegerDivision } from '../../src/rules/noIntegerDivision.ts'; -import { ruleTester } from '../utils/ruleTester.ts'; - -describe('noIntegerDivision', () => { - ruleTester.run('noIntegerDivision', noIntegerDivision, { - valid: ['1 / 2', 'd.u32(d.u32(1) / d.u32(2))'], - invalid: [ - { - code: 'd.u32(1) / 2', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / 2' } }], - }, - { - code: '1 / d.u32(2)', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: '1 / d.u32(2)' } }], - }, - { - code: 'd.u32(1) / d.u32(2)', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.u32(2)' } }], - }, - { - code: 'd.i32(1) / d.i32(2)', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.i32(1) / d.i32(2)' } }], - }, - { - code: 'd.u32(1) / d.i32(2)', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.i32(2)' } }], - }, - { - code: 'u32(1) / u32(2)', - errors: [{ messageId: 'suspiciousDivision', data: { snippet: 'u32(1) / u32(2)' } }], - }, - { - code: 'd.u32(1) / d.u32(2) / d.u32(3)', - errors: [ - { - messageId: 'suspiciousDivision', - data: { snippet: 'd.u32(1) / d.u32(2) / d.u32(3)' }, - }, - { messageId: 'suspiciousDivision', data: { snippet: 'd.u32(1) / d.u32(2)' } }, - ], - }, - ], - }); -}); diff --git a/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts b/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts deleted file mode 100644 index b06d16a33a..0000000000 --- a/packages/eslint-plugin-internal/tests/rules/noInvalidAssignment.test.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { describe } from 'vitest'; -import { ruleTester } from '../utils/ruleTester.ts'; -import { noInvalidAssignment } from '../../src/rules/noInvalidAssignment.ts'; - -describe('noInvalidAssignment', () => { - ruleTester.run('noInvalidAssignment', noInvalidAssignment, { - valid: [ - // not inside 'use gpu' function - 'const fn = (a) => { a = {}; }', - 'const fn = (a) => { a.prop = 1; }', - "const fn = (a) => { a['prop'] = 1; }", - 'const fn = (a) => { a[0] = 1; }', - - // not using parameter - "const fn = (a) => { 'use gpu'; let b = 0; b = 1; }", - "const fn = (a) => { 'use gpu'; { let a = 1; a = 2; } }", - - // correctly accessed - "const fn = (a) => { 'use gpu'; a.$ = 1 }", - "const fn = (a) => { 'use gpu'; a.$++; }", - "const fn = (a) => { 'use gpu'; a.$ += 1; }", - ], - invalid: [ - { - code: "const fn = (a) => { 'use gpu'; a = 1; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "let a; const fn = (a) => { 'use gpu'; a = 1; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a) => { 'use gpu'; a.prop = 1; }", - errors: [ - { - messageId: 'parameterAssignment', - data: { snippet: 'a.prop' }, - }, - ], - }, - { - code: "const fn = (a) => { 'use gpu'; a['prop'] = 1; }", - errors: [ - { - messageId: 'parameterAssignment', - data: { snippet: "a['prop']" }, - }, - ], - }, - { - code: "const fn = (a) => { 'use gpu'; a[0] = 1; }", - errors: [ - { - messageId: 'parameterAssignment', - data: { snippet: 'a[0]' }, - }, - ], - }, - { - code: "const fn = (a) => { 'use gpu'; a++; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a) => { 'use gpu'; --a; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = ({a}) => { 'use gpu'; a = 1; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a) => { 'use gpu'; a += 1; }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a) => { 'use gpu'; a.prop1.prop2 = 1; }", - errors: [ - { - messageId: 'parameterAssignment', - data: { snippet: 'a.prop1.prop2' }, - }, - ], - }, - { - code: "const fn = (a) => { 'use gpu'; if (true) { a = 1; } }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a) => { 'use gpu'; a = 1; { let a; } }", - errors: [{ messageId: 'parameterAssignment', data: { snippet: 'a' } }], - }, - { - code: "const fn = (a, b) => { 'use gpu'; a = 1; b = 2; }", - errors: [ - { messageId: 'parameterAssignment', data: { snippet: 'a' } }, - { messageId: 'parameterAssignment', data: { snippet: 'b' } }, - ], - }, - { - code: "const fn = (a) => { 'use gpu'; a.$prop = 1; }", - errors: [ - { - messageId: 'parameterAssignment', - data: { snippet: 'a.$prop' }, - }, - ], - }, - ], - }); - - ruleTester.run('invalidAssignment', noInvalidAssignment, { - valid: [ - // not inside 'use gpu' function - 'let a; const fn = () => { a = 1 }', - 'const outer = (a) => { const fn = () => { a = 1 } }', - 'const vars = []; const fn = () => { vars[0] = 1 }', - - // correctly accessed - "const buffer = {}; const fn = () => { 'use gpu'; buffer.$ = 1 }", - "const outer = (buffer) => { const fn = () => { 'use gpu'; buffer.$ = 1 } }", - "const buffers = []; const fn = () => { 'use gpu'; buffers[0].$ = 1 }", - ], - invalid: [ - { - code: "let a; const fn = () => { 'use gpu'; a = 1 }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "var a; const fn = () => { 'use gpu'; a = 1 }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "const outer = (a) => { const fn = () => { 'use gpu'; a = 1 } }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "const a = {}; const fn = () => { 'use gpu'; a.prop = 1; }", - errors: [ - { - messageId: 'jsAssignment', - data: { snippet: 'a.prop' }, - }, - ], - }, - { - code: "const a = {}; const fn = () => { 'use gpu'; a['prop'] = 1; }", - errors: [ - { - messageId: 'jsAssignment', - data: { snippet: "a['prop']" }, - }, - ], - }, - { - code: "const vars = []; const fn = () => { 'use gpu'; vars[0] = 1 }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'vars[0]' } }], - }, - { - code: "const fn = () => { 'use gpu'; a += 1; }; let a;", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "let a; const fn = () => { 'use gpu'; a++; }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "let a; const fn = () => { 'use gpu'; a += 1; }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "const a = {}; const fn = () => { 'use gpu'; a.prop1.prop2 = 1; }", - errors: [ - { - messageId: 'jsAssignment', - data: { snippet: 'a.prop1.prop2' }, - }, - ], - }, - { - code: "let a; const fn = () => { 'use gpu'; if (true) { a = 1; } }", - errors: [{ messageId: 'jsAssignment', data: { snippet: 'a' } }], - }, - { - code: "let a, b; const fn = () => { 'use gpu'; a = 1; b = 2; }", - errors: [ - { messageId: 'jsAssignment', data: { snippet: 'a' } }, - { messageId: 'jsAssignment', data: { snippet: 'b' } }, - ], - }, - { - code: "const a = {}; const fn = () => { 'use gpu'; a.$prop = 1; }", - errors: [ - { - messageId: 'jsAssignment', - data: { snippet: 'a.$prop' }, - }, - ], - }, - { - code: "const fn = () => { 'use gpu'; globalThis.prop = 1 }", - errors: [ - { - messageId: 'jsAssignment', - data: { snippet: 'globalThis.prop' }, - }, - ], - }, - ], - }); -}); diff --git a/packages/eslint-plugin-internal/tests/rules/noMath.test.ts b/packages/eslint-plugin-internal/tests/rules/noMath.test.ts deleted file mode 100644 index 3ddd2708f8..0000000000 --- a/packages/eslint-plugin-internal/tests/rules/noMath.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe } from 'vitest'; -import { ruleTester } from '../utils/ruleTester.ts'; -import { noMath } from '../../src/rules/noMath.ts'; - -describe('noMath', () => { - ruleTester.run('noMath', noMath, { - valid: [ - 'const result = Math.sin(1);', - 'const t = std.sin(Math.PI)', - "const fn = () => { 'use gpu'; const vec = std.sin(Math.PI); }", - "const Math = { sin: std.sin }; const fn = () => { 'use gpu'; const vec = Math.sin(0); }", - "import Math from 'utils'; const fn = () => { 'use gpu'; const vec = Math.sin(0); }", - ], - invalid: [ - { - code: "const fn = () => { 'use gpu'; const vec = Math.sin(0); }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: 'Math.sin(0)' }, - }, - ], - }, - ], - }); -}); diff --git a/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts b/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts deleted file mode 100644 index 96dc6ca5c3..0000000000 --- a/packages/eslint-plugin-internal/tests/rules/noUninitializedVariables.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe } from 'vitest'; -import { ruleTester } from '../utils/ruleTester.ts'; -import { noUninitializedVariables } from '../../src/rules/noUninitializedVariables.ts'; - -describe('noUninitializedVariables', () => { - ruleTester.run('noUninitializedVariables', noUninitializedVariables, { - 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/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts b/packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts deleted file mode 100644 index 1ca1e919a5..0000000000 --- a/packages/eslint-plugin-internal/tests/rules/noUnwrappedObjects.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe } from 'vitest'; -import { ruleTester } from '../utils/ruleTester.ts'; -import { noUnwrappedObjects } from '../../src/rules/noUnwrappedObjects.ts'; - -describe('noUnwrappedObjects', () => { - ruleTester.run('noUnwrappedObjects', noUnwrappedObjects, { - valid: [ - // correctly wrapped - "function func() { 'use gpu'; const wrapped = Schema({ a: 1 }); }", - "const func = function() { 'use gpu'; const wrapped = Schema({ a: 1 }); }", - "() => { 'use gpu'; const wrapped = Schema({ a: 1 }); }", - - // not inside 'use gpu' function - 'const pojo = { a: 1 };', - 'function func() { const unwrapped = { a: 1 }; }', - 'const func = function () { const unwrapped = { a: 1 }; }', - '() => { const unwrapped = { a: 1 }; }', - 'function func() { return { a: 1 }; }', - 'const func = function () { return { a: 1 }; }', - '() => { return { a: 1 }; }', - - // return from 'use gpu' function - "function func() { 'use gpu'; return { a: 1 }; }", - "const func = function() { 'use gpu'; return { a: 1 }; }", - "() => { 'use gpu'; return { a: 1 }; }", - "() => { 'use gpu'; return { a: { b: 1 } }; }", - "() => { 'use gpu'; return { a: 1 } as typeof Struct; }", - "() => { 'use gpu'; return { a: 1 } satisfies Struct; }", - "() => { 'use gpu'; return ({ a: 1 }); }", - ], - invalid: [ - { - code: "function func() { 'use gpu'; const unwrapped = { a: 1 }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '{ a: 1 }' }, - }, - ], - }, - { - code: "const func = function() { 'use gpu'; const unwrapped = { a: 1 }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '{ a: 1 }' }, - }, - ], - }, - { - code: "() => { 'use gpu'; const unwrapped = { a: 1 }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '{ a: 1 }' }, - }, - ], - }, - { - code: "function func() { 'unknown directive'; 'use gpu'; const unwrapped = { a: 1 }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '{ a: 1 }' }, - }, - ], - }, - { - code: "() => { 'use gpu'; const unwrapped = { a: { b: 1 } }; }", - errors: [ - { - messageId: 'unexpected', - data: { snippet: '{ a: { b: 1 } }' }, - }, - ], - }, - ], - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8d9288ee0..2575719857 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ importers: eslint-plugin-eslint-plugin: specifier: ^7.3.2 version: 7.3.2(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-internal: + specifier: workspace:* + version: link:packages/eslint-plugin-internal eslint-plugin-typegpu: specifier: workspace:* version: link:packages/eslint-plugin @@ -430,6 +433,34 @@ importers: specifier: ^4.0.17 version: 4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1))(vitest@4.1.2))(esbuild@0.27.4)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + packages/eslint-plugin-internal: + dependencies: + '@typescript-eslint/utils': + specifier: ^8.57.2 + version: 8.57.2(eslint@9.39.2(jiti@2.6.1))(tsover@5.9.11) + devDependencies: + '@types/node': + specifier: ^24.1.0 + version: 24.10.0 + '@typescript-eslint/rule-tester': + specifier: ^8.57.2 + version: 8.57.2(eslint@9.39.2(jiti@2.6.1))(tsover@5.9.11) + eslint: + specifier: ^9.39.2 + version: 9.39.2(jiti@2.6.1) + eslint-doc-generator: + specifier: ^3.3.2 + version: 3.3.2(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0)(tsover@5.9.11) + tsdown: + specifier: ^0.20.3 + version: 0.20.3(tsover@5.9.11) + typescript: + specifier: npm:tsover@^5.9.11 + version: tsover@5.9.11 + vitest: + specifier: ^4.0.17 + version: 4.1.2(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + packages/tgpu-dev-cli: dependencies: arg: From 2916f6725b65b24c3a0391a744fbb68491084a7f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:16:09 +0200 Subject: [PATCH 03/12] Implement noUselessPathSegments --- oxlint.config.ts | 2 +- .../.eslint-doc-generatorrc.mjs | 9 --- packages/eslint-plugin-internal/package.json | 3 +- packages/eslint-plugin-internal/src/index.ts | 4 +- .../src/rules/noMath.ts | 29 --------- .../src/rules/noUselessPathSegments.ts | 50 ++++++++++++++ .../tests/rules/noUselessPathSegments.test.ts | 65 +++++++++++++++++++ 7 files changed, 120 insertions(+), 42 deletions(-) delete mode 100644 packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs delete mode 100644 packages/eslint-plugin-internal/src/rules/noMath.ts create mode 100644 packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts diff --git a/oxlint.config.ts b/oxlint.config.ts index 07521e72e9..6e02ac71c1 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ 'eslint-plugin-import/no-named-as-default': 'off', 'eslint-plugin-import/no-named-as-default-member': 'off', 'eslint-plugin-import/extensions': ['error', 'always', { ignorePackages: true }], - 'eslint-plugin-internal/no-math': 'error', + 'eslint-plugin-internal/no-useless-path-segments': 'error', }, ignorePatterns: ['**/*.astro', '**/*.mjs'], overrides: [ diff --git a/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs b/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs deleted file mode 100644 index 0604c2e099..0000000000 --- a/packages/eslint-plugin-internal/.eslint-doc-generatorrc.mjs +++ /dev/null @@ -1,9 +0,0 @@ -const config = { - ignoreConfig: ['all'], - configEmoji: [['recommended', '⭐']], - postprocess: (content) => { - return content.replaceAll('💼', '🚨').replaceAll('🚫', '💤'); - }, -}; - -export default config; diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 0ed80ab20d..53c342f4f9 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -19,7 +19,8 @@ } }, "scripts": { - "test:types": "pnpm tsc --p ./tsconfig.json --noEmit" + "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", + "test": "vitest" }, "dependencies": { "@typescript-eslint/utils": "^8.57.2" diff --git a/packages/eslint-plugin-internal/src/index.ts b/packages/eslint-plugin-internal/src/index.ts index 5e4ceb23ff..229a218fbc 100644 --- a/packages/eslint-plugin-internal/src/index.ts +++ b/packages/eslint-plugin-internal/src/index.ts @@ -1,6 +1,6 @@ import pkg from '../package.json' with { type: 'json' }; import type { TSESLint } from '@typescript-eslint/utils'; -import { noMath } from './rules/noMath.ts'; +import { noUselessPathSegments } from './rules/noUselessPathSegments.ts'; const plugin = { meta: { @@ -8,7 +8,7 @@ const plugin = { version: pkg.version, }, rules: { - 'no-math': noMath, + 'no-useless-path-segments': noUselessPathSegments, }, } satisfies TSESLint.FlatConfig.Plugin; diff --git a/packages/eslint-plugin-internal/src/rules/noMath.ts b/packages/eslint-plugin-internal/src/rules/noMath.ts deleted file mode 100644 index 1a79bfb9dc..0000000000 --- a/packages/eslint-plugin-internal/src/rules/noMath.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createRule } from '../ruleCreator.ts'; - -export const noMath = createRule({ - name: 'no-math', - meta: { - type: 'suggestion', - docs: { - description: `Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions`, - }, - messages: { - unexpected: - "Using Math methods, such as '{{snippet}}', may not work as expected. Use 'std' instead", - }, - schema: [], - }, - defaultOptions: [], - - create(context) { - return { - CallExpression(node) { - context.report({ - node, - messageId: 'unexpected', - data: { snippet: context.sourceCode.getText(node) }, - }); - }, - }; - }, -}); diff --git a/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts new file mode 100644 index 0000000000..3268555b63 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts @@ -0,0 +1,50 @@ +import { createRule } from '../ruleCreator.ts'; +import * as path from 'node:path'; + +export const noUselessPathSegments = createRule({ + name: 'no-useless-path-segments', + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Disallow redundant parent folder lookups in relative import paths', + }, + messages: { + redundant: "Import path '{{path}}' can be simplified to '{{simplified}}'", + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value; + if (!importPath.startsWith('.')) { + return; + } + + const filename = context.filename; // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts` + const dir = path.dirname(filename); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests` + const resolved = path.resolve(dir, importPath); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/src/data/index.ts` + let simplified = path.relative(dir, resolved); // e.g. `../src/data/index.ts`, or `subfolder/helper.ts` + + if (!simplified.startsWith('..')) { + simplified = `./${simplified}`; + } + + if (importPath !== simplified) { + context.report({ + node, + messageId: 'redundant', + data: { path: importPath, simplified }, + fix(fixer) { + const quote = context.sourceCode.getText(node.source)[0]; + return fixer.replaceText(node.source, `${quote}${simplified}${quote}`); + }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts new file mode 100644 index 0000000000..fe2f73e6e6 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts @@ -0,0 +1,65 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noUselessPathSegments } from '../../src/rules/noUselessPathSegments.ts'; + +const filename = '/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts'; + +describe('noRedundantImportPath', () => { + ruleTester.run('noRedundantImportPath', noUselessPathSegments, { + valid: [ + { code: "import item from './file.ts';", filename }, + { code: "import item from '../file.ts';", filename }, + { code: "import item from '../../file.ts';", filename }, + { code: "import item from '../folder/file.ts';", filename }, + + { code: "import item from 'eslint-plugin-typegpu';", filename }, + { code: "import item from '@eslint-plugin/typegpu';", filename }, + ], + invalid: [ + { + code: "import item from '../tests/file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../tests/file.ts', simplified: './file.ts' }, + }, + ], + output: "import item from './file.ts';", + }, + { + code: 'import item from "../tests/file.ts";', + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../tests/file.ts', simplified: './file.ts' }, + }, + ], + output: 'import item from "./file.ts";', + }, + { + code: "import item from './../file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: './../file.ts', simplified: '../file.ts' }, + }, + ], + output: "import item from '../file.ts';", + }, + { + code: "import item from '../../typegpu/folder/file.ts';", + filename, + errors: [ + { + messageId: 'redundant', + data: { path: '../../typegpu/folder/file.ts', simplified: '../folder/file.ts' }, + }, + ], + output: "import item from '../folder/file.ts';", + }, + ], + }); +}); From f2b4ca96bc68add38bb0f0048b0d85eda7c88ffa Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:26:53 +0200 Subject: [PATCH 04/12] Implement noLongImports rule --- oxlint.config.ts | 4 +++ packages/eslint-plugin-internal/src/index.ts | 2 ++ .../src/rules/noLongImports.ts | 33 +++++++++++++++++++ .../tests/rules/noLongImports.test.ts | 27 +++++++++++++++ .../tests/rules/noUselessPathSegments.test.ts | 4 +-- 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin-internal/src/rules/noLongImports.ts create mode 100644 packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts diff --git a/oxlint.config.ts b/oxlint.config.ts index 6e02ac71c1..eb317c814a 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -46,6 +46,10 @@ export default defineConfig({ ...(eslintPlugin.configs.recommended.rules as Record), }, }, + { + files: ['apps/typegpu-docs/src/examples/**/*.ts'], + rules: { 'eslint-plugin-internal/no-long-imports': 'error' }, + }, ], env: { builtin: true, diff --git a/packages/eslint-plugin-internal/src/index.ts b/packages/eslint-plugin-internal/src/index.ts index 229a218fbc..5fd5be5883 100644 --- a/packages/eslint-plugin-internal/src/index.ts +++ b/packages/eslint-plugin-internal/src/index.ts @@ -1,6 +1,7 @@ import pkg from '../package.json' with { type: 'json' }; import type { TSESLint } from '@typescript-eslint/utils'; import { noUselessPathSegments } from './rules/noUselessPathSegments.ts'; +import { noLongImports } from './rules/noLongImports.ts'; const plugin = { meta: { @@ -9,6 +10,7 @@ const plugin = { }, rules: { 'no-useless-path-segments': noUselessPathSegments, + 'no-long-imports': noLongImports, }, } satisfies TSESLint.FlatConfig.Plugin; diff --git a/packages/eslint-plugin-internal/src/rules/noLongImports.ts b/packages/eslint-plugin-internal/src/rules/noLongImports.ts new file mode 100644 index 0000000000..982fc1b5f1 --- /dev/null +++ b/packages/eslint-plugin-internal/src/rules/noLongImports.ts @@ -0,0 +1,33 @@ +import { createRule } from '../ruleCreator.ts'; + +export const noLongImports = createRule({ + name: 'no-long-imports', + meta: { + type: 'suggestion', + fixable: 'code', + docs: { + description: 'Disallow long import paths (to be used in TypeGPU examples), except common.', + }, + messages: { + unexpected: + "Import path '{{path}}' probably won't work on StackBlitz, use imports from packages instead", + }, + schema: [], + }, + defaultOptions: [], + + create(context) { + return { + ImportDeclaration(node) { + const importPath = node.source.value; + if (importPath.startsWith('../../') && !importPath.startsWith('../../common')) { + context.report({ + node, + messageId: 'unexpected', + data: { path: importPath }, + }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts new file mode 100644 index 0000000000..69c89e7be8 --- /dev/null +++ b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts @@ -0,0 +1,27 @@ +import { describe } from 'vitest'; +import { ruleTester } from '../utils/ruleTester.ts'; +import { noLongImports } from '../../src/rules/noLongImports.ts'; + +const filename = '/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts'; + +describe('noLongImports', () => { + ruleTester.run('noLongImports', noLongImports, { + valid: [ + { code: "import item from './file.ts';", filename }, + { code: "import item from '../file.ts';", filename }, + { code: "import item from '../../common/file.ts';", filename }, + ], + invalid: [ + { + code: "import item from '../../file.ts';", + filename, + errors: [ + { + messageId: 'unexpected', + data: { path: '../../file.ts' }, + }, + ], + }, + ], + }); +}); diff --git a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts index fe2f73e6e6..1b539ca78f 100644 --- a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts @@ -4,8 +4,8 @@ import { noUselessPathSegments } from '../../src/rules/noUselessPathSegments.ts' const filename = '/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts'; -describe('noRedundantImportPath', () => { - ruleTester.run('noRedundantImportPath', noUselessPathSegments, { +describe('noUselessPathSegments', () => { + ruleTester.run('noUselessPathSegments', noUselessPathSegments, { valid: [ { code: "import item from './file.ts';", filename }, { code: "import item from '../file.ts';", filename }, From 64dc8ad8d5dbaacd14a2304af7c4461c03159239 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:31:36 +0200 Subject: [PATCH 05/12] nr fix --- packages/typegpu/src/core/pipeline/computePipeline.ts | 2 +- packages/typegpu/src/core/pipeline/renderPipeline.ts | 4 ++-- packages/typegpu/src/core/querySet/querySet.ts | 2 +- packages/typegpu/src/core/root/init.ts | 6 +----- packages/typegpu/src/core/root/rootTypes.ts | 4 ++-- packages/typegpu/src/core/slot/slotTypes.ts | 2 +- packages/typegpu/src/core/unroll/tgpuUnroll.ts | 10 +++++----- packages/typegpu/src/tgsl/generationHelpers.ts | 6 +++--- 8 files changed, 16 insertions(+), 20 deletions(-) diff --git a/packages/typegpu/src/core/pipeline/computePipeline.ts b/packages/typegpu/src/core/pipeline/computePipeline.ts index 57cd50dd0a..b61e3d8457 100644 --- a/packages/typegpu/src/core/pipeline/computePipeline.ts +++ b/packages/typegpu/src/core/pipeline/computePipeline.ts @@ -1,5 +1,5 @@ import type { AnyComputeBuiltin } from '../../builtin.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import { type ResolvedSnippet, snip } from '../../data/snippet.ts'; import { sizeOf } from '../../data/sizeOf.ts'; import type { AnyWgslData } from '../../data/wgslTypes.ts'; diff --git a/packages/typegpu/src/core/pipeline/renderPipeline.ts b/packages/typegpu/src/core/pipeline/renderPipeline.ts index 2bdfebb6dd..b7c6f14430 100644 --- a/packages/typegpu/src/core/pipeline/renderPipeline.ts +++ b/packages/typegpu/src/core/pipeline/renderPipeline.ts @@ -1,6 +1,6 @@ import type { AnyBuiltin, OmitBuiltins } from '../../builtin.ts'; -import type { IndexFlag, TgpuBuffer, VertexFlag } from '../../core/buffer/buffer.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { IndexFlag, TgpuBuffer, VertexFlag } from '../buffer/buffer.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import { isBuiltin } from '../../data/attributes.ts'; import { type Disarray, getCustomLocation, type UndecorateRecord } from '../../data/dataTypes.ts'; import { sizeOf } from '../../data/sizeOf.ts'; diff --git a/packages/typegpu/src/core/querySet/querySet.ts b/packages/typegpu/src/core/querySet/querySet.ts index 547f337fbe..9d1322e3df 100644 --- a/packages/typegpu/src/core/querySet/querySet.ts +++ b/packages/typegpu/src/core/querySet/querySet.ts @@ -1,5 +1,5 @@ import { setName, type TgpuNamable } from '../../shared/meta.ts'; -import type { ExperimentalTgpuRoot } from '../../core/root/rootTypes.ts'; +import type { ExperimentalTgpuRoot } from '../root/rootTypes.ts'; import { $internal } from '../../shared/symbols.ts'; export interface TgpuQuerySet extends TgpuNamable { diff --git a/packages/typegpu/src/core/root/init.ts b/packages/typegpu/src/core/root/init.ts index 8fad9a5951..51aee4a391 100644 --- a/packages/typegpu/src/core/root/init.ts +++ b/packages/typegpu/src/core/root/init.ts @@ -1,9 +1,5 @@ import { type AnyComputeBuiltin, builtin, type OmitBuiltins } from '../../builtin.ts'; -import { - INTERNAL_createQuerySet, - isQuerySet, - type TgpuQuerySet, -} from '../../core/querySet/querySet.ts'; +import { INTERNAL_createQuerySet, isQuerySet, type TgpuQuerySet } from '../querySet/querySet.ts'; import type { AnyData, Disarray } from '../../data/dataTypes.ts'; import type { AnyWgslData, BaseData, v3u, Vec3u, WgslArray } from '../../data/wgslTypes.ts'; import { invariant } from '../../errors.ts'; diff --git a/packages/typegpu/src/core/root/rootTypes.ts b/packages/typegpu/src/core/root/rootTypes.ts index 05b9c3db32..5a2b1167ec 100644 --- a/packages/typegpu/src/core/root/rootTypes.ts +++ b/packages/typegpu/src/core/root/rootTypes.ts @@ -1,5 +1,5 @@ import type { AnyComputeBuiltin, AnyFragmentInputBuiltin, OmitBuiltins } from '../../builtin.ts'; -import type { TgpuQuerySet } from '../../core/querySet/querySet.ts'; +import type { TgpuQuerySet } from '../querySet/querySet.ts'; import type { AnyData, Disarray, UndecorateRecord } from '../../data/dataTypes.ts'; import type { WgslComparisonSamplerProps, WgslSamplerProps } from '../../data/sampler.ts'; import type { @@ -51,7 +51,7 @@ import type { LayoutToAllowedAttribs, } from '../vertexLayout/vertexAttribute.ts'; import type { TgpuVertexLayout } from '../vertexLayout/vertexLayout.ts'; -import type { TgpuComputeFn } from './../function/tgpuComputeFn.ts'; +import type { TgpuComputeFn } from '../function/tgpuComputeFn.ts'; import type { TgpuNamable } from '../../shared/meta.ts'; import type { AnyAutoCustoms, diff --git a/packages/typegpu/src/core/slot/slotTypes.ts b/packages/typegpu/src/core/slot/slotTypes.ts index 80cab9197a..8fce92c90d 100644 --- a/packages/typegpu/src/core/slot/slotTypes.ts +++ b/packages/typegpu/src/core/slot/slotTypes.ts @@ -5,7 +5,7 @@ import type { GPUValueOf, Infer, InferGPU } from '../../shared/repr.ts'; import { $gpuValueOf, $internal, $providing } from '../../shared/symbols.ts'; import type { UnwrapRuntimeConstructor } from '../../tgpuBindGroupLayout.ts'; import type { TgpuBufferShorthand } from '../buffer/bufferShorthand.ts'; -import type { TgpuBufferUsage } from './../buffer/bufferUsage.ts'; +import type { TgpuBufferUsage } from '../buffer/bufferUsage.ts'; import type { TgpuConst } from '../constant/tgpuConstant.ts'; import type { Withable } from '../root/rootTypes.ts'; import type { TgpuTextureView } from '../texture/texture.ts'; diff --git a/packages/typegpu/src/core/unroll/tgpuUnroll.ts b/packages/typegpu/src/core/unroll/tgpuUnroll.ts index 5f17ff0c2f..1b30fae7e2 100644 --- a/packages/typegpu/src/core/unroll/tgpuUnroll.ts +++ b/packages/typegpu/src/core/unroll/tgpuUnroll.ts @@ -1,9 +1,9 @@ import { stitch } from '../resolve/stitch.ts'; -import { $gpuCallable, $internal, $resolve } from '../../../src/shared/symbols.ts'; -import { setName } from '../../../src/shared/meta.ts'; -import type { DualFn } from '../../../src/types.ts'; -import { type ResolvedSnippet, snip, type Snippet } from '../../../src/data/snippet.ts'; -import type { ResolutionCtx, SelfResolvable } from '../../../src/types.ts'; +import { $gpuCallable, $internal, $resolve } from '../../shared/symbols.ts'; +import { setName } from '../../shared/meta.ts'; +import type { DualFn } from '../../types.ts'; +import { type ResolvedSnippet, snip, type Snippet } from '../../data/snippet.ts'; +import type { ResolutionCtx, SelfResolvable } from '../../types.ts'; import type { BaseData } from '../../data/wgslTypes.ts'; /** diff --git a/packages/typegpu/src/tgsl/generationHelpers.ts b/packages/typegpu/src/tgsl/generationHelpers.ts index 628742269a..76835e7c33 100644 --- a/packages/typegpu/src/tgsl/generationHelpers.ts +++ b/packages/typegpu/src/tgsl/generationHelpers.ts @@ -26,9 +26,9 @@ import { type SelfResolvable, } from '../types.ts'; import type { ShelllessRepository } from './shellless.ts'; -import { stitch } from '../../src/core/resolve/stitch.ts'; -import { WgslTypeError } from '../../src/errors.ts'; -import { $internal, $resolve } from '../../src/shared/symbols.ts'; +import { stitch } from '../core/resolve/stitch.ts'; +import { WgslTypeError } from '../errors.ts'; +import { $internal, $resolve } from '../shared/symbols.ts'; export function numericLiteralToSnippet(value: number): Snippet { if (value >= 2 ** 63 || value < -(2 ** 63)) { From d4f5c1cff72d6843ce6438bdc35cd879360293ca Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:39:20 +0200 Subject: [PATCH 06/12] Fix lockfile --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1c0e2a0c4..cb799d73dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -431,7 +431,7 @@ importers: version: tsover@5.9.11 vitest: specifier: ^4.0.17 - version: 4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) + version: 4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3))(vitest@4.1.2))(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) packages/eslint-plugin-internal: dependencies: @@ -459,7 +459,7 @@ importers: version: tsover@5.9.11 vitest: specifier: ^4.0.17 - version: 4.1.2(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.4)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.1.2(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3)) packages/tgpu-dev-cli: dependencies: @@ -16092,7 +16092,7 @@ snapshots: optionalDependencies: vite: 8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3) - vitest@4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2)(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3): + vitest@4.0.18(@types/node@24.10.0)(@vitest/browser-preview@4.1.2(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3))(vitest@4.1.2))(esbuild@0.27.5)(jiti@2.6.1)(jsdom@27.0.0(canvas@3.2.0)(postcss@8.5.8))(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(msw@2.10.2(@types/node@24.10.0)(tsover@5.9.11))(vite@8.0.2(@types/node@24.10.0)(esbuild@0.27.5)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.3)) From 9b70f146fa07369020a144bf626b084cc714ff8b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:51:49 +0200 Subject: [PATCH 07/12] Update readme --- packages/eslint-plugin-internal/README.md | 48 +---------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/packages/eslint-plugin-internal/README.md b/packages/eslint-plugin-internal/README.md index fae1227e74..4381d343a0 100644 --- a/packages/eslint-plugin-internal/README.md +++ b/packages/eslint-plugin-internal/README.md @@ -1,51 +1,7 @@
-# eslint-plugin-typegpu +# eslint-plugin-internal -TypeGPU specific linting rules for ESLint. - -[Docs](https://docs.swmansion.com/TypeGPU/tooling/eslint-plugin-typegpu/) -- [GitHub](https://github.com/software-mansion/TypeGPU/tree/main/packages/eslint-plugin) -- [npm](https://www.npmjs.com/package/eslint-plugin-typegpu) +Internal ESLint rules used by this repository.
- -## Installation - -`npm add -D eslint-plugin-typegpu` - -After installing, the plugin needs to be configured. - -## Configuration - -Configuration depends on the linter used. - -In eslint, either define the used rules manually, or use one of the configs provided by the plugin. - -```ts -import { defineConfig } from "eslint/config"; -import typegpu from "eslint-plugin-typegpu"; - -export default defineConfig([ -// other configs - typegpu.configs.recommended, -]); -``` - -`eslint-plugin-typegpu` provides two configs: `all` (enabled on all rules) and `recommended`. - -## List of supported rules - - - -🚨 Configurations enabled in.\ -⚠️ Configurations set to warn in.\ -⭐ Set in the `recommended` configuration. - -| Name                       | Description | 🚨 | ⚠️ | -| :--------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ | :- | :- | -| [no-integer-division](docs/rules/no-integer-division.md) | Disallow division incorporating numbers wrapped in 'u32' and 'i32' | | ⭐ | -| [no-invalid-assignment](docs/rules/no-invalid-assignment.md) | Disallow assignments that will generate invalid WGSL | ⭐ | | -| [no-math](docs/rules/no-math.md) | Disallow usage of JavaScript 'Math' methods inside 'use gpu' functions | | ⭐ | -| [no-uninitialized-variables](docs/rules/no-uninitialized-variables.md) | Disallow variable declarations without initializers inside 'use gpu' functions | ⭐ | | -| [no-unwrapped-objects](docs/rules/no-unwrapped-objects.md) | Disallow unwrapped Plain Old JavaScript Objects inside 'use gpu' functions (except returns) | ⭐ | | - - From 3d5d4202efecef99f0d432e7c9b2b08c77d87832 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:55:48 +0200 Subject: [PATCH 08/12] Make tests more portable --- .../tests/rules/noLongImports.test.ts | 9 +++------ .../tests/rules/noUselessPathSegments.test.ts | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts index 69c89e7be8..5ed1e99489 100644 --- a/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/noLongImports.test.ts @@ -2,19 +2,16 @@ import { describe } from 'vitest'; import { ruleTester } from '../utils/ruleTester.ts'; import { noLongImports } from '../../src/rules/noLongImports.ts'; -const filename = '/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts'; - describe('noLongImports', () => { ruleTester.run('noLongImports', noLongImports, { valid: [ - { code: "import item from './file.ts';", filename }, - { code: "import item from '../file.ts';", filename }, - { code: "import item from '../../common/file.ts';", filename }, + { code: "import item from './file.ts';" }, + { code: "import item from '../file.ts';" }, + { code: "import item from '../../common/file.ts';" }, ], invalid: [ { code: "import item from '../../file.ts';", - filename, errors: [ { messageId: 'unexpected', diff --git a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts index 1b539ca78f..db67522d87 100644 --- a/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts +++ b/packages/eslint-plugin-internal/tests/rules/noUselessPathSegments.test.ts @@ -1,8 +1,9 @@ import { describe } from 'vitest'; import { ruleTester } from '../utils/ruleTester.ts'; import { noUselessPathSegments } from '../../src/rules/noUselessPathSegments.ts'; +import path from 'path'; -const filename = '/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts'; +const filename = path.join(process.cwd(), 'packages', 'typegpu', 'tests', 'buffer.test.ts'); describe('noUselessPathSegments', () => { ruleTester.run('noUselessPathSegments', noUselessPathSegments, { From c6f1c5742f217ddc608c8f9ecd3675ad354b008f Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:57:38 +0200 Subject: [PATCH 09/12] Add windows compat --- .../eslint-plugin-internal/src/rules/noUselessPathSegments.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts index 3268555b63..5e9717d007 100644 --- a/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts +++ b/packages/eslint-plugin-internal/src/rules/noUselessPathSegments.ts @@ -27,7 +27,9 @@ export const noUselessPathSegments = createRule({ const filename = context.filename; // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests/buffer.test.ts` const dir = path.dirname(filename); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/tests` const resolved = path.resolve(dir, importPath); // e.g. `/Users/me/typegpu-monorepo/packages/typegpu/src/data/index.ts` - let simplified = path.relative(dir, resolved); // e.g. `../src/data/index.ts`, or `subfolder/helper.ts` + let simplified = path + .relative(dir, resolved) // e.g. `../src/data/index.ts`, or `subfolder/helper.ts` + .replaceAll('\\', '/'); // Windows compatibility if (!simplified.startsWith('..')) { simplified = `./${simplified}`; From f375895614d28cd13699cc3da826740aac808056 Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:38:39 +0200 Subject: [PATCH 10/12] Remove the fix tag --- packages/eslint-plugin-internal/src/rules/noLongImports.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/eslint-plugin-internal/src/rules/noLongImports.ts b/packages/eslint-plugin-internal/src/rules/noLongImports.ts index 982fc1b5f1..936164240c 100644 --- a/packages/eslint-plugin-internal/src/rules/noLongImports.ts +++ b/packages/eslint-plugin-internal/src/rules/noLongImports.ts @@ -4,7 +4,6 @@ export const noLongImports = createRule({ name: 'no-long-imports', meta: { type: 'suggestion', - fixable: 'code', docs: { description: 'Disallow long import paths (to be used in TypeGPU examples), except common.', }, @@ -20,7 +19,7 @@ export const noLongImports = createRule({ return { ImportDeclaration(node) { const importPath = node.source.value; - if (importPath.startsWith('../../') && !importPath.startsWith('../../common')) { + if (importPath.startsWith('../../') && !importPath.startsWith('../../common/')) { context.report({ node, messageId: 'unexpected', From 4c4a9159ccce457fbacefa72b2002746e406cabf Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:45:16 +0200 Subject: [PATCH 11/12] Simplify package.json --- packages/eslint-plugin-internal/package.json | 19 +------------------ packages/eslint-plugin-internal/tsconfig.json | 2 +- .../eslint-plugin-internal/tsdown.config.ts | 9 --------- pnpm-lock.yaml | 6 ------ 4 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 packages/eslint-plugin-internal/tsdown.config.ts diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 53c342f4f9..b7cadb80a9 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,23 +1,9 @@ { "name": "eslint-plugin-internal", - "version": "0.10.0", "private": true, - "description": "Ruleset for working on TypeGPU monorepo", "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/software-mansion/TypeGPU.git#main", - "directory": "packages/eslint-plugin-internal" - }, "type": "module", - "sideEffects": false, "main": "./src/index.ts", - "exports": { - ".": { - "types": "./src/index.ts", - "import": "./src/index.ts" - } - }, "scripts": { "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", "test": "vitest" @@ -29,13 +15,10 @@ "@types/node": "catalog:types", "@typescript-eslint/rule-tester": "^8.57.2", "eslint": "^9.39.2", - "eslint-doc-generator": "^3.3.2", - "tsdown": "^0.20.3", "typescript": "^5.9.3", "vitest": "^4.0.17" }, "peerDependencies": { "eslint": "^9.0.0" - }, - "packageManager": "pnpm@10.15.1" + } } diff --git a/packages/eslint-plugin-internal/tsconfig.json b/packages/eslint-plugin-internal/tsconfig.json index 3a544d3dd8..88024df5ec 100644 --- a/packages/eslint-plugin-internal/tsconfig.json +++ b/packages/eslint-plugin-internal/tsconfig.json @@ -4,5 +4,5 @@ "types": ["node"] }, "include": ["src/**/*", "tests/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules"] } diff --git a/packages/eslint-plugin-internal/tsdown.config.ts b/packages/eslint-plugin-internal/tsdown.config.ts deleted file mode 100644 index 3ec1155cc3..0000000000 --- a/packages/eslint-plugin-internal/tsdown.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'tsdown'; - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['esm'], - dts: true, - clean: true, - sourcemap: true, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb799d73dc..d6cc58d7e8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -448,12 +448,6 @@ importers: eslint: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) - eslint-doc-generator: - specifier: ^3.3.2 - version: 3.3.2(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.0)(tsover@5.9.11) - tsdown: - specifier: ^0.20.3 - version: 0.20.3(tsover@5.9.11) typescript: specifier: npm:tsover@^5.9.11 version: tsover@5.9.11 From 8baf753096e32cbe2cccb03bbf2cfb839876ca6b Mon Sep 17 00:00:00 2001 From: Aleksander Katan <56294622+aleksanderkatan@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:48:38 +0200 Subject: [PATCH 12/12] Readd version to package.json --- packages/eslint-plugin-internal/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index b7cadb80a9..e2b1dc379a 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,5 +1,6 @@ { "name": "eslint-plugin-internal", + "version": "0.10.0", "private": true, "license": "MIT", "type": "module",