diff --git a/.README/README.md b/.README/README.md
index 667187d..630f660 100644
--- a/.README/README.md
+++ b/.README/README.md
@@ -92,6 +92,7 @@ See [ESLint documentation](https://eslint.org/docs/user-guide/configuring/config
{"gitdown": "include", "file": "./rules/id-match.md"}
{"gitdown": "include", "file": "./rules/import-specifier-newline.md"}
{"gitdown": "include", "file": "./rules/no-restricted-strings.md"}
+{"gitdown": "include", "file": "./rules/no-unused-exports.md"}
{"gitdown": "include", "file": "./rules/no-use-extend-native.md"}
{"gitdown": "include", "file": "./rules/prefer-import-alias.md"}
{"gitdown": "include", "file": "./rules/prefer-inline-type-import.md"}
diff --git a/.README/rules/no-unused-exports.md b/.README/rules/no-unused-exports.md
new file mode 100644
index 0000000..89cf2ff
--- /dev/null
+++ b/.README/rules/no-unused-exports.md
@@ -0,0 +1,13 @@
+### `no-unused-exports`
+
+Identifies unused exports.
+
+> **Note** This rule uses [`ts-unused-exports`](https://github.com/pzavolinsky/ts-unused-exports) program behind the scenes.
+
+#### Options
+
+|Config|Type|Description|
+|---|---|---|
+|`tsConfigPath`|string|Path to [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)]
+
+
diff --git a/README.md b/README.md
index dec0b87..64dd15a 100644
--- a/README.md
+++ b/README.md
@@ -305,6 +305,24 @@ Disallow specified strings.
The 1st option is an array of strings that cannot be contained in the codebase.
+
+
+### no-unused-exports
+
+Identifies unused exports.
+
+> **Note** This rule uses [`ts-unused-exports`](https://github.com/pzavolinsky/ts-unused-exports) program behind the scenes.
+
+
+
+#### Options
+
+|Config|Type|Description|
+|---|---|---|
+|`tsConfigPath`|string|Path to [tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)]
+
+
+
### no-use-extend-native
@@ -382,8 +400,8 @@ Note: This rule is equivalent to [`sort-keys`](https://eslint.org/docs/rules/sor
This rule requires identifiers in assignments and `function` definitions to match a specified regular expression.
-
-
+
+
#### Options
The 1st option is "asc" or "desc".
diff --git a/package-lock.json b/package-lock.json
index 6012579..b7205b0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,7 @@
"is-proto-prop": "^2.0.0",
"lodash": "^4.17.21",
"natural-compare": "^1.4.0",
+ "ts-unused-exports": "^9.0.3",
"xregexp": "^5.1.1"
},
"devDependencies": {
@@ -1814,8 +1815,7 @@
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"node_modules/@types/minimist": {
"version": "1.2.2",
@@ -8206,7 +8206,6 @@
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
- "dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -13592,7 +13591,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true,
"engines": {
"node": ">=4"
}
@@ -13943,11 +13941,97 @@
"node": ">=0.3.1"
}
},
+ "node_modules/ts-unused-exports": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-9.0.3.tgz",
+ "integrity": "sha512-LCGLYL0EVdXNj1O/cGfpP2Fx+zfqoV936iMyIhvSVnXk4RUjwmSjMzzCNXI9b1j9PCs946a2TbRMhJh7/XUyUA==",
+ "dependencies": {
+ "chalk": "^4.0.0",
+ "tsconfig-paths": "^3.9.0"
+ },
+ "bin": {
+ "ts-unused-exports": "bin/ts-unused-exports"
+ },
+ "funding": {
+ "url": "https://github.com/pzavolinsky/ts-unused-exports?sponsor=1"
+ },
+ "peerDependencies": {
+ "typescript": ">=3.8.3"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/ts-unused-exports/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/ts-unused-exports/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ts-unused-exports/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/ts-unused-exports/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/ts-unused-exports/node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ts-unused-exports/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
- "dev": true,
"dependencies": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
@@ -13959,7 +14043,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
- "dev": true,
"dependencies": {
"minimist": "^1.2.0"
},
@@ -14051,7 +14134,6 @@
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
"integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
- "dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15970,8 +16052,7 @@
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
- "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
- "dev": true
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="
},
"@types/minimist": {
"version": "1.2.2",
@@ -20681,8 +20762,7 @@
"minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
- "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
- "dev": true
+ "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"minimist-options": {
"version": "4.1.0",
@@ -24546,8 +24626,7 @@
"strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
- "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
- "dev": true
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="
},
"strip-final-newline": {
"version": "2.0.0",
@@ -24794,11 +24873,64 @@
}
}
},
+ "ts-unused-exports": {
+ "version": "9.0.3",
+ "resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-9.0.3.tgz",
+ "integrity": "sha512-LCGLYL0EVdXNj1O/cGfpP2Fx+zfqoV936iMyIhvSVnXk4RUjwmSjMzzCNXI9b1j9PCs946a2TbRMhJh7/XUyUA==",
+ "requires": {
+ "chalk": "^4.0.0",
+ "tsconfig-paths": "^3.9.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
"tsconfig-paths": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz",
"integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==",
- "dev": true,
"requires": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
@@ -24810,7 +24942,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
- "dev": true,
"requires": {
"minimist": "^1.2.0"
}
@@ -24884,8 +25015,7 @@
"typescript": {
"version": "4.9.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz",
- "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==",
- "dev": true
+ "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg=="
},
"uglify-js": {
"version": "3.17.4",
diff --git a/package.json b/package.json
index 7a355da..599e23b 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,7 @@
"is-proto-prop": "^2.0.0",
"lodash": "^4.17.21",
"natural-compare": "^1.4.0",
+ "ts-unused-exports": "^9.0.3",
"xregexp": "^5.1.1"
},
"description": "Canonical linting rules for ESLint.",
diff --git a/src/index.ts b/src/index.ts
index 5b2e6ac..725612b 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,6 +7,7 @@ import filenameNoIndex from './rules/filenameNoIndex';
import idMatch from './rules/idMatch';
import importSpecifierNewline from './rules/importSpecifierNewline';
import noRestrictedStrings from './rules/noRestrictedStrings';
+import noUnusedExports from './rules/noUnusedExports';
import noUseExtendNative from './rules/noUseExtendNative';
import preferImportAlias from './rules/preferImportAlias';
import preferInlineTypeImport from './rules/preferInlineTypeImport';
@@ -26,6 +27,7 @@ export = {
'id-match': idMatch,
'import-specifier-newline': importSpecifierNewline,
'no-restricted-strings': noRestrictedStrings,
+ 'no-unused-exports': noUnusedExports,
'no-use-extend-native': noUseExtendNative,
'prefer-import-alias': preferImportAlias,
'prefer-inline-type-import': preferInlineTypeImport,
diff --git a/src/rules/noUnusedExports.ts b/src/rules/noUnusedExports.ts
new file mode 100644
index 0000000..ee723c2
--- /dev/null
+++ b/src/rules/noUnusedExports.ts
@@ -0,0 +1,78 @@
+import analyzeTsConfig from 'ts-unused-exports';
+import { createRule } from '../utilities';
+
+const defaultOptions = {
+ tsConfigPath: '',
+};
+
+type Options = [
+ {
+ tsConfigPath: string;
+ },
+];
+
+type MessageIds = 'unusedExport';
+
+export default createRule({
+ create: (context) => {
+ const [options] = context.options;
+
+ const result = analyzeTsConfig(options.tsConfigPath);
+
+ return {
+ Program() {
+ const filename = context.getFilename();
+
+ if (!result[filename]) {
+ return;
+ }
+
+ const sourceCode = context.getSourceCode();
+
+ for (const unusedExport of result[filename]) {
+ const index = sourceCode.getIndexFromLoc({
+ column: unusedExport.location.character,
+ line: unusedExport.location.line,
+ });
+
+ const exportToken = sourceCode.getTokenByRangeStart(index);
+
+ if (!exportToken) {
+ throw new Error('Expected export node');
+ }
+
+ context.report({
+ data: {
+ exportName: unusedExport.exportName,
+ },
+ messageId: 'unusedExport',
+ node: exportToken,
+ });
+ }
+ },
+ };
+ },
+ defaultOptions: [defaultOptions],
+ meta: {
+ docs: {
+ description: 'Identifies unused exports.',
+ recommended: false,
+ },
+ messages: {
+ unusedExport: "Export '{{exportName}}' is unused.",
+ },
+ schema: [
+ {
+ additionalProperties: false,
+ properties: {
+ tsConfigPath: {
+ type: 'string',
+ },
+ },
+ type: 'object',
+ },
+ ],
+ type: 'layout',
+ },
+ name: 'no-unused-exports',
+});
diff --git a/tests/fixtures/noUnusedExports/barUser.ts b/tests/fixtures/noUnusedExports/barUser.ts
new file mode 100644
index 0000000..bde92b4
--- /dev/null
+++ b/tests/fixtures/noUnusedExports/barUser.ts
@@ -0,0 +1,4 @@
+import { BAR } from './usedBar';
+
+// eslint-disable-next-line no-console
+console.log(BAR);
diff --git a/tests/fixtures/noUnusedExports/tsconfig.json b/tests/fixtures/noUnusedExports/tsconfig.json
new file mode 100644
index 0000000..102110d
--- /dev/null
+++ b/tests/fixtures/noUnusedExports/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "include": [
+ ".",
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/noUnusedExports/unusedFoo.ts b/tests/fixtures/noUnusedExports/unusedFoo.ts
new file mode 100644
index 0000000..4083521
--- /dev/null
+++ b/tests/fixtures/noUnusedExports/unusedFoo.ts
@@ -0,0 +1 @@
+export const FOO = '';
diff --git a/tests/fixtures/noUnusedExports/usedBar.ts b/tests/fixtures/noUnusedExports/usedBar.ts
new file mode 100644
index 0000000..6e168b3
--- /dev/null
+++ b/tests/fixtures/noUnusedExports/usedBar.ts
@@ -0,0 +1 @@
+export const BAR = '';
diff --git a/tests/rules/assertions/noUnusedExports.ts b/tests/rules/assertions/noUnusedExports.ts
new file mode 100644
index 0000000..f8c9b08
--- /dev/null
+++ b/tests/rules/assertions/noUnusedExports.ts
@@ -0,0 +1,34 @@
+import { readFileSync } from 'node:fs';
+import path from 'node:path';
+
+const fixturesPath = path.resolve(__dirname, '../../fixtures/noUnusedExports');
+
+export default {
+ invalid: [
+ {
+ code: readFileSync(path.resolve(fixturesPath, 'unusedFoo.ts'), 'utf8'),
+ errors: [
+ {
+ message: "Export 'FOO' is unused.",
+ },
+ ],
+ filename: path.resolve(fixturesPath, 'unusedFoo.ts'),
+ options: [
+ {
+ tsConfigPath: path.resolve(fixturesPath, 'tsconfig.json'),
+ },
+ ],
+ },
+ ],
+ valid: [
+ {
+ code: readFileSync(path.resolve(fixturesPath, 'usedBar.ts'), 'utf8'),
+ filename: path.resolve(fixturesPath, 'usedBar.ts'),
+ options: [
+ {
+ tsConfigPath: path.resolve(fixturesPath, 'tsconfig.json'),
+ },
+ ],
+ },
+ ],
+};
diff --git a/tests/rules/index.ts b/tests/rules/index.ts
index 473e50d..781d18b 100644
--- a/tests/rules/index.ts
+++ b/tests/rules/index.ts
@@ -19,6 +19,7 @@ const reportingRules = [
'id-match',
'import-specifier-newline',
'no-restricted-strings',
+ 'no-unused-exports',
'no-use-extend-native',
'prefer-import-alias',
'prefer-inline-type-import',