-
-
Notifications
You must be signed in to change notification settings - Fork 70
dx: Lint rule for imports #2345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
c9f0366
3c52c10
2916f67
f2b4ca9
64dc8ad
93d0dda
d4f5c1c
9b70f14
3d5d420
c6f1c57
f375895
4c4a915
8baf753
45956ad
bd62170
0092351
05e8514
6edb951
1679918
f930784
3750c24
59a922d
9cb6f6e
ecc418c
f087658
041bdef
44064ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| <div align="center"> | ||
|
|
||
| # eslint-plugin-internal | ||
|
|
||
| Internal ESLint rules used by this repository. | ||
|
|
||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| { | ||
| "name": "eslint-plugin-internal", | ||
| "version": "0.10.0", | ||
| "private": true, | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "main": "./src/index.ts", | ||
| "scripts": { | ||
| "test:types": "pnpm tsc --p ./tsconfig.json --noEmit", | ||
| "test": "vitest" | ||
| }, | ||
| "dependencies": { | ||
| "@typescript-eslint/utils": "^8.57.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "catalog:types", | ||
| "@typescript-eslint/rule-tester": "^8.57.2", | ||
| "eslint": "^9.39.2", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.0.17" | ||
|
aleksanderkatan marked this conversation as resolved.
Outdated
|
||
| }, | ||
| "peerDependencies": { | ||
| "eslint": "^9.0.0" | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| 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: { | ||
| name: pkg.name, | ||
| version: pkg.version, | ||
| }, | ||
| rules: { | ||
| 'no-useless-path-segments': noUselessPathSegments, | ||
| 'no-long-imports': noLongImports, | ||
| }, | ||
| } satisfies TSESLint.FlatConfig.Plugin; | ||
|
|
||
| export default plugin; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { ESLintUtils } from '@typescript-eslint/utils'; | ||
|
|
||
| export const createRule = ESLintUtils.RuleCreator( | ||
| () => `https://docs.swmansion.com/TypeGPU/getting-started/`, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { createRule } from '../ruleCreator.ts'; | ||
|
|
||
| export const noLongImports = createRule({ | ||
| name: 'no-long-imports', | ||
| meta: { | ||
| type: 'suggestion', | ||
| 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/')) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this introduce a pretty high chance of false positives?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only enable this rule on example files, where this will not work due to stackblitz pathing requirements
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not |
||
| context.report({ | ||
| node, | ||
| messageId: 'unexpected', | ||
| data: { path: importPath }, | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| 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('.')) { | ||
|
cieplypolar marked this conversation as resolved.
|
||
| 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` | ||
| .replaceAll('\\', '/'); // Windows compatibility | ||
|
|
||
| if (!simplified.startsWith('..')) { | ||
| simplified = `./${simplified}`; | ||
| } | ||
|
aleksanderkatan marked this conversation as resolved.
|
||
|
|
||
| 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}`); | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| }; | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import { describe } from 'vitest'; | ||
| import { ruleTester } from '../utils/ruleTester.ts'; | ||
| import { noLongImports } from '../../src/rules/noLongImports.ts'; | ||
|
|
||
| describe('noLongImports', () => { | ||
| ruleTester.run('noLongImports', noLongImports, { | ||
| valid: [ | ||
| { 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';", | ||
| errors: [ | ||
| { | ||
| messageId: 'unexpected', | ||
| data: { path: '../../file.ts' }, | ||
| }, | ||
| ], | ||
| }, | ||
| ], | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { describe } from 'vitest'; | ||
| import { ruleTester } from '../utils/ruleTester.ts'; | ||
| import { noUselessPathSegments } from '../../src/rules/noUselessPathSegments.ts'; | ||
| import path from 'path'; | ||
|
|
||
| const filename = path.join(process.cwd(), 'packages', 'typegpu', 'tests', 'buffer.test.ts'); | ||
|
aleksanderkatan marked this conversation as resolved.
|
||
|
|
||
| describe('noUselessPathSegments', () => { | ||
| ruleTester.run('noUselessPathSegments', noUselessPathSegments, { | ||
| valid: [ | ||
|
cieplypolar marked this conversation as resolved.
|
||
| { 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';", | ||
| }, | ||
| ], | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "types": ["node"] | ||
| }, | ||
| "include": ["src/**/*", "tests/**/*"], | ||
| "exclude": ["node_modules"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { defineConfig } from 'vitest/config'; | ||
|
|
||
| export default defineConfig({ | ||
| test: { | ||
| include: ['tests/**/*.test.ts'], | ||
| environment: 'node', | ||
| }, | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.