Repository: sweepline/eslint-plugin-unused-imports Branch: master Commit: 853a372a6713 Files: 23 Total size: 24.8 KB Directory structure: gitextract_523dtdv8/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── release.yml │ └── run_test.yml ├── .gitignore ├── .prettierrc.js ├── .vscode/ │ └── launch.json ├── LICENSE ├── README.md ├── docs/ │ └── rules/ │ ├── no-unused-imports.md │ └── no-unused-vars.md ├── package.json ├── pnpm-workspace.yaml ├── src/ │ ├── __test__/ │ │ ├── cases-js.ts │ │ ├── cases-ts.ts │ │ └── no-unused-imports.test.ts │ ├── index.ts │ └── rules/ │ ├── load-rule.ts │ ├── no-unused-imports.ts │ ├── no-unused-vars.ts │ └── predicates.ts ├── tsconfig.json ├── tsup.config.ts └── vitest.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "" labels: "" assignees: "" --- # Please follow the general troubleshooting steps first: If the issue is with something being marked wrongly as a unused import and therefore being removed. It is an issue with the imported package (`@typescript-eslint/eslint-plugin` for TS or `eslint` for JS) and its `no-unused-vars` rule. I cannot do anything about this except updating if a fix is made upstream. If new rules are added `no-unused-vars` upstream which should be autofixed, mark your issue _rule addition_. Now if something is not marked an import and being removed by the autofixer, it is an issue I can do something about. Please replace the above with a brief summary of your issue. ### Features: **Please note by far the quickest way to get a new feature is to file a Pull Request.** ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - 'v*' jobs: release: uses: sxzz/workflows/.github/workflows/release.yml@v1 with: publish: true permissions: contents: write id-token: write ================================================ FILE: .github/workflows/run_test.yml ================================================ # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs name: Node.js CI on: push: branches: [ "master" ] pull_request: branches: [ "master" ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [18.x, 20.x] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: pnpm install - run: pnpm run build - run: pnpm test ================================================ FILE: .gitignore ================================================ node_modules .idea dist ================================================ FILE: .prettierrc.js ================================================ module.exports = { tabWidth: 4, semi: true, // Trailing semicolons trailingComma: "all", singleQuote: false, quoteProps: "as-needed", bracketSpacing: true, printWidth: 100, // Line width (this fit my 1440p screen at a half-screen window) arrowParens: "always", }; ================================================ FILE: .vscode/launch.json ================================================ { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.1.0", "configurations": [ { "type": "node", "request": "launch", "name": "Debug current test file", "runtimeExecutable": "npm", "runtimeArgs": ["test", "--testPathPattern", "${file}", "--coverage", "false"], "port": 9229, "cwd": "${fileDirname}", "timeout": 10000, "console": "integratedTerminal" } ] } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Mikkel Holmer Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # eslint-plugin-unused-imports Find and remove unused es6 module imports. It works by splitting up the `no-unused-vars` rule depending on it being an import statement in the AST and providing an autofix rule to remove the nodes if they are imports. This plugin composes the rule `no-unused-vars` of either the typescript or js plugin so be aware that the other plugins needs to be installed and reporting correctly for this to do so. ## _Versions_ - Version 4.1.x is for eslint 9 with @typescript-eslint/eslint-plugin 5 - 8 - Version 4.0.x is for eslint 9 with @typescript-eslint/eslint-plugin 8 - Version 3.x.x is for eslint 8 with @typescript-eslint/eslint-plugin 6 - 7 - Version 2.x.x is for eslint 8 with @typescript-eslint/eslint-plugin 5 - Version 1.x.x is for eslint 6 and 7. ## Typescript If running typescript with [@typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) make sure to use both `@typescript-eslint/eslint-plugin` and `@typescript-eslint/parser`. ## React If writing react code you need to install `eslint-plugin-react` and enable the two rules `react/jsx-uses-react` and `react/jsx-uses-vars`. Otherwise all imports for components will be reported unused. ## Installation You'll first need to install [ESLint](http://eslint.org) (and [@typescript-eslint](https://github.com/typescript-eslint/typescript-eslint) if using typescript): ```bash npm i eslint --save-dev ``` Next, install `eslint-plugin-unused-imports`: ```bash npm install eslint-plugin-unused-imports --save-dev ``` **Note:** If you installed ESLint globally (using the `-g` flag) then you must also install `eslint-plugin-unused-imports` globally. ## Usage Add `unused-imports` to the plugins section of your `eslint.config.js` configuration file. ```js import unusedImports from "eslint-plugin-unused-imports"; export default [{ plugins: { "unused-imports": unusedImports, }, rules: { "no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off", "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ "warn", { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_", }, ] } }]; ``` ## Supported Rules - `no-unused-imports` - `no-unused-vars` ================================================ FILE: docs/rules/no-unused-imports.md ================================================ # Do not allow unused imports (no-unused-imports) A rule to find unused-imports only, as well as an autofixer. ================================================ FILE: docs/rules/no-unused-vars.md ================================================ # Do not allow unused vars (no-unused-vars) This is just a helper rule to filter out the things caught by the `no-unused-imports` rule without double warnings. As well as being able to set them at different warning levels. ================================================ FILE: package.json ================================================ { "name": "eslint-plugin-unused-imports", "version": "4.4.1", "type": "commonjs", "packageManager": "pnpm@10.18.3", "description": "Report and remove unused es6 modules", "keywords": [ "eslint", "eslintplugin", "eslint-plugin", "import", "unused", "modules", "autofix" ], "author": "Mikkel Holmer Pedersen", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.js" } }, "files": [ "dist" ], "scripts": { "build": "tsup", "test": "vitest", "format": "prettier --write '**/*.[jt]s'", "prepack": "npm run build", "release": "bumpp" }, "peerDependencies": { "@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0", "eslint": "^10.0.0 || ^9.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@typescript-eslint/eslint-plugin": { "optional": true } }, "devDependencies": { "@types/eslint": "^9.6.1", "@typescript-eslint-v5/eslint-plugin": "npm:@typescript-eslint/eslint-plugin@^5.62.0", "@typescript-eslint-v5/parser": "npm:@typescript-eslint/parser@^5.62.0", "@typescript-eslint-v6/eslint-plugin": "npm:@typescript-eslint/eslint-plugin@^6.21.0", "@typescript-eslint-v6/parser": "npm:@typescript-eslint/parser@^6.21.0", "@typescript-eslint-v7/eslint-plugin": "npm:@typescript-eslint/eslint-plugin@^7.18.0", "@typescript-eslint-v7/parser": "npm:@typescript-eslint/parser@^7.18.0", "@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/utils": "^8.46.1", "bumpp": "^10.3.1", "eslint": "^9.37.0", "prettier": "^3.6.2", "tsup": "^8.5.0", "typescript": "^5.9.3", "typescript-eslint": "^8.46.1", "vitest": "^3.2.4" }, "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/sweepline/eslint-plugin-unused-imports.git" }, "homepage": "https://github.com/sweepline/eslint-plugin-unused-imports", "bugs": "https://github.com/sweepline/eslint-plugin-unused-imports/issues" } ================================================ FILE: pnpm-workspace.yaml ================================================ onlyBuiltDependencies: - esbuild ================================================ FILE: src/__test__/cases-js.ts ================================================ export default { valid: [ { code: ` import x from "package"; import { a, b } from "./utils"; import y from "package"; const c = a() + b + x() + y(); `, }, { code: ` import { NoAuthenticationGuard } from "./no-authentication.guard"; import { JwtAuthenticationGuard } from "./jwt-authentication.guard"; /** * You can reference {@link NoAuthenticationGuard} instead. * It's recommended to use the {@link JwtAuthenticationGuard}. */ const LOCAL_ENVIRONMENT_AUTHENTICATION_GUARD = JwtAuthenticationGuard; `, }, { code: ` import { SomeClass } from "./some-class"; /** * @see SomeClass */ const example = "test"; `, }, { code: ` import { MyType } from "./types"; /** * @type {MyType} */ let value; `, }, { code: ` import { Config } from "./config"; /** * @param {Config} config - The configuration object */ function setup(config) {} `, }, ], invalid: [ { code: ` import x from "package"; import { a, b } from "./utils"; import y from "package"; const c = b(x, y); `, errors: ["'a' is defined but never used."], output: ` import x from "package"; import { b } from "./utils"; import y from "package"; const c = b(x, y); `, }, { code: ` import { a, b } from "./utils"; import y from "package"; /** * this is a jsdoc! */ const c = a(y); `, errors: ["'b' is defined but never used."], output: ` import { a } from "./utils"; import y from "package"; /** * this is a jsdoc! */ const c = a(y); `, }, { code: ` import { a } from "./utils"; import y from "package"; const c = 4; console.log(y); `, errors: ["'a' is defined but never used."], output: ` import y from "package"; const c = 4; console.log(y); `, }, { code: ` import y from "package"; import { a } from "./utils"; /** * c is the number 4 */ const c = 4; console.log(y); `, errors: ["'a' is defined but never used."], output: ` import y from "package"; /** * c is the number 4 */ const c = 4; console.log(y); `, }, { code: ` import { UnusedClass } from "./unused"; import { UsedInJSDoc } from "./used"; /** * Reference to {@link UsedInJSDoc} */ const example = "test"; `, errors: ["'UnusedClass' is defined but never used."], output: ` import { UsedInJSDoc } from "./used"; /** * Reference to {@link UsedInJSDoc} */ const example = "test"; `, }, ], }; ================================================ FILE: src/__test__/cases-ts.ts ================================================ export default { valid: [ { code: ` import x from "package"; import { a, b } from "./utils"; import y from "package"; import TType from "Package"; const c: TType = a() + b + x() + y(); `, }, { code: ` import { NoAuthenticationGuard } from "./no-authentication.guard"; import { JwtAuthenticationGuard } from "./jwt-authentication.guard"; /** * You can reference {@link NoAuthenticationGuard} instead. * It's recommended to use the {@link JwtAuthenticationGuard}. */ const LOCAL_ENVIRONMENT_AUTHENTICATION_GUARD = JwtAuthenticationGuard; `, }, { code: ` import type { SomeType } from "./types"; /** * @see SomeType for more details */ const example = "test"; `, }, { code: ` import type { Config } from "./config"; /** * @param {Config} config - The configuration object */ function setup(config: any) {} `, }, ], invalid: [ { code: ` import x from "package"; import { a, b } from "./utils"; import y from "package"; import TType from "Package"; const c = a() + b + x() + y(); `, errors: ["'TType' is defined but never used."], output: ` import x from "package"; import { a, b } from "./utils"; import y from "package"; const c = a() + b + x() + y(); `, }, { code: ` import type { UnusedType } from "./unused"; import type { UsedInJSDoc } from "./used"; /** * Reference to {@link UsedInJSDoc} */ const example = "test"; `, errors: ["'UnusedType' is defined but never used."], output: ` import type { UsedInJSDoc } from "./used"; /** * Reference to {@link UsedInJSDoc} */ const example = "test"; `, }, ], }; ================================================ FILE: src/__test__/no-unused-imports.test.ts ================================================ import { RuleTester } from "eslint"; import casesJS from "./cases-js"; import casesTS from "./cases-ts"; import { it, describe } from "vitest"; import { createRuleWithPredicate, unusedImportsPredicate } from "../rules/predicates"; import { getESLintBaseRule } from "../rules/load-rule"; const parsers = [ { name: "eslint", parser: undefined, rule: getESLintBaseRule(), }, { name: "typescript-eslint v8", parser: await import("@typescript-eslint/parser").then((r) => r.default), rule: await import("@typescript-eslint/eslint-plugin").then( (r) => r.rules["no-unused-vars"], ), }, { name: "typescript-eslint v7", parser: await import("@typescript-eslint-v7/parser").then((r) => r.default), rule: await import("@typescript-eslint-v7/eslint-plugin").then( (r) => r.rules["no-unused-vars"], ), }, { name: "typescript-eslint v6", parser: await import("@typescript-eslint-v6/parser").then((r) => r.default), rule: await import("@typescript-eslint-v6/eslint-plugin").then( (r) => r.rules["no-unused-vars"], ), }, { name: "typescript-eslint v5", parser: await import("@typescript-eslint-v5/parser").then((r) => r.default), rule: await import("@typescript-eslint-v5/eslint-plugin").then( (r) => r.rules["no-unused-vars"], ), }, ]; describe("no-unused-imports", () => { for (const { name, parser, rule: baseRule } of parsers) { it(`with ${name}`, () => { const ruleTester = new RuleTester({ languageOptions: { parser, ecmaVersion: 2015, sourceType: "module", }, }); const rule = createRuleWithPredicate( "no-unused-imports", baseRule, unusedImportsPredicate, ); ruleTester.run("no-unused-imports", rule, casesJS); if (name !== "eslint") { ruleTester.run("no-unused-imports", rule, casesTS); } }); } }); ================================================ FILE: src/index.ts ================================================ /** * @fileoverview Find and remove unused es6 modules * @author Mikkel Holmer Pedersen */ import noUnusedVars from "./rules/no-unused-vars"; import noUnusedImports from "./rules/no-unused-imports"; import { ESLint } from "eslint"; const plugin: ESLint.Plugin = { meta: { name: "unused-imports", }, rules: { "no-unused-vars": noUnusedVars, "no-unused-imports": noUnusedImports, }, }; export default plugin; ================================================ FILE: src/rules/load-rule.ts ================================================ import { Rule } from "eslint"; import { createRequire } from "module"; let rule: Rule.RuleModule | undefined; const require = createRequire(import.meta.url); export function getBaseRule(): Rule.RuleModule { if (!rule) { rule = getRuleFromTSLintPlugin() ?? getRuleFromTSLint() ?? getESLintBaseRule(); } return rule!; } export function getRuleFromTSLintPlugin() { try { const tslintPlugin = require("@typescript-eslint/eslint-plugin"); return tslintPlugin.rules["no-unused-vars"]; } catch (_) { return null; } } export function getRuleFromTSLint() { try { const tslint = require("typescript-eslint"); return tslint.plugin.rules["no-unused-vars"]; } catch (_) { return null; } } export function getESLintBaseRule() { try { const eslint = require("eslint"); return new eslint.Linter({ configType: "eslintrc" }).getRules().get("no-unused-vars"); } catch { // since ESLint 10.0.0, new Linter({ configType: "eslintrc" }) will now throw an TypeError explicitly: // https://github.com/eslint/eslint/blob/b69cfb32a16c5d5e9986390d484fae1d21e406f9/lib/linter/linter.js#L757 // // This means we can catch the error and load the rule from an unsafe API: const eslint_USE_AT_YOUR_OWN_RISK = require("eslint/use-at-your-own-risk"); // builtinRules was added since ESLint 8.0.0 and was mentioned in ESLint's "Migrate to ESLint 8.0.0" guide: // https://eslint.org/docs/latest/use/migrate-to-8.0.0#-the-cliengine-class-has-been-removed // // However, it's still considered an unstable API and may change without a major version bump. We need to guard the access. if ('builtinRules' in eslint_USE_AT_YOUR_OWN_RISK && eslint_USE_AT_YOUR_OWN_RISK.builtinRules instanceof Map) { return eslint_USE_AT_YOUR_OWN_RISK.builtinRules.get("no-unused-vars"); } throw new TypeError("[eslint-plugin-unused-imports] Cannot load 'no-unused-vars' rule from ESLint. This is most likely due to a breaking change in ESLint's internal API. Please report this issue to 'eslint-plugin-unused-imports'."); } } ================================================ FILE: src/rules/no-unused-imports.ts ================================================ /** * @fileoverview Add fixer to imports in no-unused-vars. * @author Mikkel Holmer Pedersen */ import { unusedImportsPredicate, createRuleWithPredicate } from "./predicates"; import { getBaseRule } from "./load-rule"; export default createRuleWithPredicate("no-unused-imports", getBaseRule(), unusedImportsPredicate); ================================================ FILE: src/rules/no-unused-vars.ts ================================================ /** * @fileoverview Filter imports from no-unused-vars. * @author Mikkel Holmer Pedersen */ import { createRuleWithPredicate, unusedVarsPredicate } from "./predicates"; import { getBaseRule } from "./load-rule"; export default createRuleWithPredicate("no-unused-vars", getBaseRule(), unusedVarsPredicate); ================================================ FILE: src/rules/predicates.ts ================================================ import { SourceCode, AST, Rule } from "eslint"; export type Predicate = ( problem: Rule.ReportDescriptor, context: Rule.RuleContext, ) => Rule.ReportDescriptor | false; const commaFilter = { filter: (token: AST.Token) => token.value === "," }; const includeCommentsFilter = { includeComments: true }; /** * Check if an identifier is referenced in JSDoc comments * Looks for JSDoc tags like @link, @see, @type, etc. */ function isUsedInJSDoc(identifierName: string, sourceCode: SourceCode): boolean { const comments = sourceCode.getAllComments(); // JSDoc tags that can reference identifiers // Pattern matches: {@link Name}, {@see Name}, @type {Name}, etc. const jsdocPattern = new RegExp( // {@link Name} or @see Name `(?:@(?:link|linkcode|linkplain|see)\\s+${identifierName}\\b)|` + // {@link Name} `(?:\\{@(?:link|linkcode|linkplain)\\s+${identifierName}\\b\\})|` + // @type {Name}, @param {Name}, etc. `(?:[@{](?:type|typedef|param|returns?|template|augments|extends|implements)\\s+[^}]*\\b${identifierName}\\b)`, ); return comments.some((comment) => { // Only check block comments (/* ... */) as JSDoc uses block comment syntax if (comment.type !== "Block") { return false; } return jsdocPattern.test(comment.value); }); } function makePredicate( isImport: boolean, addFixer?: (parent: any, sourceCode: SourceCode) => Partial | boolean, ): Predicate { return (problem, context) => { const sourceCode = context.sourceCode || context.getSourceCode(); const node = (problem as any).node ?? // typescript-eslint >= 7.8 sets a range instead of a node sourceCode.getNodeByRangeIndex(sourceCode.getIndexFromLoc((problem as any).loc.start)); const { parent } = node; // Check if this is an import and if it's used in JSDoc comments if (parent && /^Import(|Default|Namespace)Specifier$/.test(parent.type) && isImport) { const identifierName = node.name; if (identifierName && isUsedInJSDoc(identifierName, sourceCode)) { // Don't report if used in JSDoc return false; } } return parent ? /^Import(|Default|Namespace)Specifier$/.test(parent.type) == isImport ? Object.assign(problem, addFixer?.(parent, sourceCode)) : false : isImport ? false : problem; }; } export const unusedVarsPredicate = makePredicate(false); export const unusedImportsPredicate = makePredicate(true, (parent, sourceCode) => ({ fix(fixer) { const grandParent = parent.parent; if (!grandParent) { return null; } // Only one import if (grandParent.specifiers.length === 1) { const nextToken = sourceCode.getTokenAfter(grandParent, includeCommentsFilter); const newLinesBetween = nextToken ? nextToken.loc!.start.line - grandParent.loc.start.line : 0; const endOfReplaceRange = nextToken ? nextToken.range![0] : grandParent.range[1]; const count = Math.max(0, newLinesBetween - 1); return [ fixer.remove(grandParent), fixer.replaceTextRange( [grandParent.range[1], endOfReplaceRange], "\n".repeat(count), ), ]; } // Not last specifier if (parent !== grandParent.specifiers[grandParent.specifiers.length - 1]) { const comma = sourceCode.getTokenAfter(parent, commaFilter)!; const prevNode = sourceCode.getTokenBefore(parent)!; return [ fixer.removeRange([prevNode.range[1], parent.range![0]]), fixer.remove(parent), fixer.remove(comma), ]; } // Default export and a single normal left, ex. "import default, { package1 } from 'module';" if ( grandParent.specifiers.filter((specifier) => specifier.type === "ImportSpecifier") .length === 1 ) { const start = sourceCode.getTokenBefore(parent, commaFilter)!; const end = sourceCode.getTokenAfter(parent, { filter: (token) => token.value === "}", })!; return fixer.removeRange([start.range[0], end.range[1]]); } return fixer.removeRange([ sourceCode.getTokenBefore(parent, commaFilter)!.range[0], parent.range[1], ]); }, })); export function createRuleWithPredicate( name: string, baseRule: Rule.RuleModule, predicate: Predicate, ): Rule.RuleModule { return { ...baseRule, meta: { ...baseRule.meta, fixable: "code", docs: { ...baseRule.meta?.docs, url: `https://github.com/sweepline/eslint-plugin-unused-imports/blob/master/docs/rules/${name}.md`, }, }, create(context) { return baseRule.create( Object.create(context, { report: { enumerable: true, value(problem: Rule.ReportDescriptor) { const result = predicate(problem, context); if (result) { context.report(result); } }, }, }), ); }, }; } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "skipLibCheck": true, "noEmit": true, } } ================================================ FILE: tsup.config.ts ================================================ import { defineConfig } from "tsup"; export default defineConfig({ entryPoints: ["src/index.ts"], format: ["cjs", "esm"], dts: true, clean: true, splitting: true, shims: true, cjsInterop: true, external: ["eslint", "@typescript-eslint/eslint-plugin"], }); ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from "vitest/config"; export default defineConfig({ test: { globals: true, }, });