@jeremybarbet/oxlint-plugin 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,8 +35,9 @@ pass as every other lint rule — rather than as a batch report from a separate
35
35
  Not reported:
36
36
 
37
37
  - Exports tagged `/** @public */` or `/** @beta */`, the same opt-out knip honours.
38
- - Exports consumed inside their own file. Those are over-exposed, not dead — the fix is dropping the
39
- `export` keyword, not deleting the declaration. Most exported return types look like this.
38
+ - Exports consumed inside their own file. Those are over-exposed, not dead: the fix is dropping the
39
+ `export` keyword, not deleting the declaration. Most exported return types look like this, and
40
+ `no-unnecessary-export` below is the rule that reports them.
40
41
  - Anything matching `**/*.config.*`, always. A config file is loaded by path by the tool that owns
41
42
  it, so its default export is an entry point by definition.
42
43
 
@@ -57,6 +58,38 @@ Not reported:
57
58
  `ignoreDefaultExports` is the narrow version of `ignoreExports`: listing `app/**` there would also
58
59
  silence the named exports in those files, so a route that grew a dead helper would go unreported.
59
60
 
61
+ ### `no-unnecessary-export`
62
+
63
+ Reports a binding that is exported but only ever used inside its own declaring file. The
64
+ complement of `no-unused-modules`, which deliberately leaves this case alone because the two want
65
+ opposite fixes:
66
+
67
+ | Rule | Condition | Fix |
68
+ | --- | --- | --- |
69
+ | `no-unused-modules` | Nothing references the export, anywhere. | Delete the declaration. |
70
+ | `no-unnecessary-export` | Only the declaring file references it. | Drop the `export` keyword. |
71
+
72
+ The two never both fire on one name. Together they mean an export is either imported somewhere or
73
+ reported.
74
+
75
+ Type-only exports are skipped by default: an exported interface used only as a local return type is
76
+ the ordinary shape of a typed codebase, not over-exposure. `@public` / `@beta` opt out, same as
77
+ `no-unused-modules`.
78
+
79
+ Reported, never autofixed. The verdict comes from a static crawl that cannot see a runtime-resolved
80
+ import, so the fix is offered as an editor suggestion and `--fix` leaves the file alone. Only a lone
81
+ `export const x = …` gets one: a specifier list drops an entry rather than the keyword, and a
82
+ destructured or multi-declarator export may still have an imported sibling.
83
+
84
+ | Option | Default | |
85
+ | --- | --- | --- |
86
+ | `includeTypes` | `false` | Also report type-only exports. |
87
+ | `ignoreExports` | `[]` | Globs whose exports are never reported. |
88
+
89
+ ```ts
90
+ 'op/no-unnecessary-export': 'error',
91
+ ```
92
+
60
93
  ### `no-unused-class-members`
61
94
 
62
95
  Reports public class members that nothing outside the declaring file references.
@@ -101,10 +134,10 @@ nested type cannot silently break Apollo's cache-control computation. Ships an a
101
134
 
102
135
  ## Project discovery
103
136
 
104
- `no-unused-modules` and `no-unused-class-members` need a whole-project view, so they crawl and parse
105
- the project once per process and answer per-file from the result. (Oxlint's plugin API is per-file
106
- with no end-of-run hook, so this is the same approach `eslint-plugin-import` takes.) It costs roughly
107
- 120ms on an 830-file repo.
137
+ `no-unused-modules`, `no-unnecessary-export` and `no-unused-class-members` need a whole-project
138
+ view, so they crawl and parse the project once per process and answer per-file from the result.
139
+ (Oxlint's plugin API is per-file with no end-of-run hook, so this is the same approach
140
+ `eslint-plugin-import` takes.) It costs roughly 120ms on an 830-file repo.
108
141
 
109
142
  Discovery is zero-config: everything under the oxlint working directory that looks like source,
110
143
  skipping `node_modules`, `dist`, `build`, `coverage`, `android`, `ios` and dotfile directories.
package/dist/index.d.mts CHANGED
@@ -12,6 +12,7 @@ declare const _default: {
12
12
  };
13
13
  rules: {
14
14
  'no-export-all': import("./types.mts").Rule;
15
+ 'no-unnecessary-export': import("./types.mts").Rule;
15
16
  'no-unused-class-members': import("./types.mts").Rule;
16
17
  'no-unused-modules': import("./types.mts").Rule;
17
18
  'no-unused-styles': import("./types.mts").Rule;
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  import noExportAll from "./rules/no-export-all.mjs";
2
+ import noUnnecessaryExport from "./rules/no-unnecessary-export.mjs";
2
3
  import noUnusedClassMembers from "./rules/no-unused-class-members.mjs";
3
4
  import noUnusedModules from "./rules/no-unused-modules.mjs";
4
5
  import noUnusedStyles from "./rules/no-unused-styles.mjs";
@@ -15,6 +16,7 @@ export default {
15
16
  meta: { name: 'op' },
16
17
  rules: {
17
18
  'no-export-all': noExportAll,
19
+ 'no-unnecessary-export': noUnnecessaryExport,
18
20
  'no-unused-class-members': noUnusedClassMembers,
19
21
  'no-unused-modules': noUnusedModules,
20
22
  'no-unused-styles': noUnusedStyles,
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,2BAA2B,CAAC;AACpD,OAAO,oBAAoB,MAAM,qCAAqC,CAAC;AACvE,OAAO,eAAe,MAAM,+BAA+B,CAAC;AAC5D,OAAO,cAAc,MAAM,8BAA8B,CAAC;AAC1D,OAAO,mBAAmB,MAAM,mCAAmC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAe;IACb,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;IACpB,KAAK,EAAE;QACL,eAAe,EAAE,WAAW;QAC5B,yBAAyB,EAAE,oBAAoB;QAC/C,mBAAmB,EAAE,eAAe;QACpC,kBAAkB,EAAE,cAAc;QAClC,uBAAuB,EAAE,mBAAmB;KAC7C;CACF,CAAC"}
1
+ {"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,2BAA2B,CAAC;AACpD,OAAO,mBAAmB,MAAM,mCAAmC,CAAC;AACpE,OAAO,oBAAoB,MAAM,qCAAqC,CAAC;AACvE,OAAO,eAAe,MAAM,+BAA+B,CAAC;AAC5D,OAAO,cAAc,MAAM,8BAA8B,CAAC;AAC1D,OAAO,mBAAmB,MAAM,mCAAmC,CAAC;AAEpE;;;;;;;GAOG;AACH,eAAe;IACb,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE;IACpB,KAAK,EAAE;QACL,eAAe,EAAE,WAAW;QAC5B,uBAAuB,EAAE,mBAAmB;QAC5C,yBAAyB,EAAE,oBAAoB;QAC/C,mBAAmB,EAAE,eAAe;QACpC,kBAAkB,EAAE,cAAc;QAClC,uBAAuB,EAAE,mBAAmB;KAC7C;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { type Rule } from '../types.mts';
2
+ declare const rule: Rule;
3
+ export default rule;
@@ -0,0 +1,127 @@
1
+ import { relative } from 'node:path';
2
+ import { matchesGlob } from "../glob.mjs";
3
+ import { getProjectIndex, readProjectSettings } from "../project-index.mjs";
4
+ import {} from "../types.mjs";
5
+ /**
6
+ * The complement of `no-unused-modules`: that rule treats an export its own file consumes as used,
7
+ * which leaves an over-exposed binding unreported. Nothing imports it, so the `export` should go.
8
+ */
9
+ /** Always applied, on top of whatever a project configures. Mirrors `no-unused-modules`. */
10
+ const ALWAYS_IGNORED_EXPORTS = ['**/*.config.*'];
11
+ /** Declarations that emit no runtime binding, so exporting one is not the over-exposure this rule means. */
12
+ const TYPE_DECLARATIONS = new Set(['TSInterfaceDeclaration', 'TSTypeAliasDeclaration']);
13
+ const rule = {
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description: 'Disallow exporting a binding only its own file uses',
18
+ },
19
+ messages: {
20
+ unnecessary: "'{{name}}' is only used in this file. Drop the `export` keyword.",
21
+ },
22
+ schema: [
23
+ {
24
+ type: 'object',
25
+ properties: {
26
+ includeTypes: { type: 'boolean' },
27
+ ignoreExports: { type: 'array', items: { type: 'string' } },
28
+ },
29
+ additionalProperties: false,
30
+ },
31
+ ],
32
+ hasSuggestions: true,
33
+ },
34
+ create(context) {
35
+ const raw = (context.options?.[0] ?? {});
36
+ const includeTypes = raw.includeTypes ?? false;
37
+ const ignoreExports = [...ALWAYS_IGNORED_EXPORTS, ...(raw.ignoreExports ?? [])];
38
+ const index = getProjectIndex(context.cwd, readProjectSettings(context));
39
+ const self = context.filename;
40
+ const file = index.files.get(self);
41
+ if (!file) {
42
+ return {};
43
+ }
44
+ const relativePath = relative(index.root, self).split('\\').join('/');
45
+ if (matchesGlob(relativePath, ignoreExports)) {
46
+ return {};
47
+ }
48
+ const importers = index.importedFrom.get(self);
49
+ // A namespace import (`import * as x`) reaches every export, so none of them can be called local.
50
+ if (importers?.has('*')) {
51
+ return {};
52
+ }
53
+ const isLocalOnly = (name) => {
54
+ if (importers?.has(name) || file.intentionalExports.has(name)) {
55
+ return false;
56
+ }
57
+ // The inversion. Without a local reference the export is dead, which is `no-unused-modules`'
58
+ // finding, not this one, and the two must never both fire on a name.
59
+ return file.localReferences.has(name);
60
+ };
61
+ /**
62
+ * The span covering the `export` keyword, or null when removing it is not the right fix. A
63
+ * specifier list drops one entry instead, and a destructured export may have an imported sibling.
64
+ */
65
+ const exportKeywordRange = (node, declaration) => {
66
+ if ((node.specifiers ?? []).length > 0) {
67
+ return null;
68
+ }
69
+ const declarations = (declaration.declarations ?? []);
70
+ const single = declaration.id?.type === 'Identifier' ||
71
+ (declarations.length === 1 && declarations[0]?.id?.type === 'Identifier');
72
+ return single ? [node.range[0], declaration.range[0]] : null;
73
+ };
74
+ const report = (node, name, range) => {
75
+ if (!isLocalOnly(name)) {
76
+ return;
77
+ }
78
+ context.report({
79
+ node,
80
+ messageId: 'unnecessary',
81
+ data: { name },
82
+ ...(range && {
83
+ suggest: [
84
+ {
85
+ desc: 'Remove the `export` keyword.',
86
+ fix: fixer => fixer.removeRange(range),
87
+ },
88
+ ],
89
+ }),
90
+ });
91
+ };
92
+ return {
93
+ /**
94
+ * `export default` is out of scope: its export name is `default`, a reserved word no
95
+ * identifier can match, so it never has a local reference to begin with.
96
+ */
97
+ ExportNamedDeclaration(node) {
98
+ if (node.source || (!includeTypes && node.exportKind === 'type')) {
99
+ return;
100
+ }
101
+ for (const specifier of (node.specifiers ?? [])) {
102
+ const name = specifier.exported?.name;
103
+ if (!name || name === 'default' || (!includeTypes && specifier.exportKind === 'type')) {
104
+ continue;
105
+ }
106
+ report(specifier.exported, name, null);
107
+ }
108
+ const declaration = node.declaration;
109
+ if (!declaration || (!includeTypes && TYPE_DECLARATIONS.has(declaration.type))) {
110
+ return;
111
+ }
112
+ const range = exportKeywordRange(node, declaration);
113
+ if (declaration.id?.name) {
114
+ report(declaration.id, declaration.id.name, range);
115
+ return;
116
+ }
117
+ for (const declarator of (declaration.declarations ?? [])) {
118
+ if (declarator.id?.type === 'Identifier') {
119
+ report(declarator.id, declarator.id.name, range);
120
+ }
121
+ }
122
+ },
123
+ };
124
+ },
125
+ };
126
+ export default rule;
127
+ //# sourceMappingURL=no-unnecessary-export.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"no-unnecessary-export.mjs","sourceRoot":"","sources":["../../src/rules/no-unnecessary-export.mts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@jeremybarbet/oxlint-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Shared oxlint JS plugin: dead-code rules that oxlint, tsc and knip each miss, plus a few house rules.",
5
5
  "author": "Jérémy Barbet <jeremgraph@gmail.com> (https://github.com/jeremybarbet)",
6
6
  "repository": "https://github.com/jeremybarbet/oxlint-plugin",
7
7
  "homepage": "https://github.com/jeremybarbet/oxlint-plugin#readme",
8
8
  "license": "MIT",
9
- "packageManager": "yarn@4.13.0",
9
+ "packageManager": "yarn@4.18.0",
10
10
  "engines": {
11
11
  "node": ">=22"
12
12
  },