@jeremybarbet/oxlint-plugin 0.1.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 +126 -0
- package/dist/glob.d.mts +1 -0
- package/dist/glob.mjs +33 -0
- package/dist/glob.mjs.map +1 -0
- package/dist/index.d.mts +21 -0
- package/dist/index.mjs +24 -0
- package/dist/index.mjs.map +1 -0
- package/dist/project-index.d.mts +37 -0
- package/dist/project-index.mjs +311 -0
- package/dist/project-index.mjs.map +1 -0
- package/dist/rules/no-export-all.d.mts +3 -0
- package/dist/rules/no-export-all.mjs +22 -0
- package/dist/rules/no-export-all.mjs.map +1 -0
- package/dist/rules/no-unused-class-members.d.mts +3 -0
- package/dist/rules/no-unused-class-members.mjs +116 -0
- package/dist/rules/no-unused-class-members.mjs.map +1 -0
- package/dist/rules/no-unused-modules.d.mts +3 -0
- package/dist/rules/no-unused-modules.mjs +115 -0
- package/dist/rules/no-unused-modules.mjs.map +1 -0
- package/dist/rules/no-unused-styles.d.mts +3 -0
- package/dist/rules/no-unused-styles.mjs +149 -0
- package/dist/rules/no-unused-styles.mjs.map +1 -0
- package/dist/rules/require-cache-control.d.mts +3 -0
- package/dist/rules/require-cache-control.mjs +105 -0
- package/dist/rules/require-cache-control.mjs.map +1 -0
- package/dist/types.d.mts +70 -0
- package/dist/types.mjs +9 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
* Neither knip nor tsc can see an unused *public class member*: knip analyses module-level exports
|
|
7
|
+
* only, and an exported, used class hides its whole body from it, while tsc's `noUnusedLocals`
|
|
8
|
+
* covers private members alone. In a NestJS codebase where nearly all logic lives on services,
|
|
9
|
+
* resolvers and processors, that is most of the code.
|
|
10
|
+
*
|
|
11
|
+
* The check is name-based rather than type-based, because oxlint exposes no type checker to JS
|
|
12
|
+
* plugins. That makes it deliberately conservative: a member whose name is referenced anywhere
|
|
13
|
+
* else — even on a different class — is treated as used, so the rule under-reports rather than
|
|
14
|
+
* pointing at live code.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_IGNORE_NAMES = [
|
|
17
|
+
'constructor',
|
|
18
|
+
/** NestJS lifecycle hooks, called by the framework and never by name. */
|
|
19
|
+
'onModuleInit',
|
|
20
|
+
'onModuleDestroy',
|
|
21
|
+
'onApplicationBootstrap',
|
|
22
|
+
'onApplicationShutdown',
|
|
23
|
+
'beforeApplicationShutdown',
|
|
24
|
+
];
|
|
25
|
+
/** Mirrors `knip --production` — code only a test reaches is still dead code. */
|
|
26
|
+
const DEFAULT_IGNORE_USAGES_IN = ['**/*.spec.ts', '**/*.test.ts', '**/*.mock.ts', 'src/test/**'];
|
|
27
|
+
const MEMBER_TYPES = new Set(['MethodDefinition', 'PropertyDefinition', 'AccessorProperty']);
|
|
28
|
+
/**
|
|
29
|
+
* A class that extends or implements anything can have its members invoked polymorphically — by a
|
|
30
|
+
* base class, or by whatever holds the interface. `R2PMTilesSource implements Source` is called
|
|
31
|
+
* entirely by the `pmtiles` library, and `UserGuard extends AuthGuard(…)` has `getRequest` called
|
|
32
|
+
* by Passport. Neither reference shows up as a member access anywhere in this repo.
|
|
33
|
+
*/
|
|
34
|
+
const isPolymorphic = (node) => Boolean(node.superClass) || (node.implements ?? []).length > 0;
|
|
35
|
+
const isReportableMember = (member, ignoreNames) => {
|
|
36
|
+
if (!MEMBER_TYPES.has(member.type) || member.computed || member.key?.type !== 'Identifier') {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
// `#x` is already covered by the compiler, and `private x` is covered by `noUnusedLocals`.
|
|
40
|
+
if (member.accessibility === 'private' || member.kind === 'constructor') {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// A decorator means something outside the type system reaches this member — a `@Cron` schedule,
|
|
44
|
+
// a GraphQL `@Field`, a `class-validator` constraint. This is what kept `deprecatedDailyClouds`
|
|
45
|
+
// correctly silent until its `@Cron` was removed.
|
|
46
|
+
if ((member.decorators ?? []).length > 0) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return !ignoreNames.includes(member.key.name);
|
|
50
|
+
};
|
|
51
|
+
const rule = {
|
|
52
|
+
meta: {
|
|
53
|
+
type: 'problem',
|
|
54
|
+
docs: {
|
|
55
|
+
description: 'Disallow public class members that nothing outside the declaring file references',
|
|
56
|
+
},
|
|
57
|
+
messages: {
|
|
58
|
+
unused: "'{{name}}' is never referenced outside {{file}}. Remove it if it is dead.",
|
|
59
|
+
privateCandidate: "'{{name}}' is only referenced inside {{file}}. Mark it `private`.",
|
|
60
|
+
},
|
|
61
|
+
schema: [
|
|
62
|
+
{
|
|
63
|
+
type: 'object',
|
|
64
|
+
properties: {
|
|
65
|
+
ignoreNames: { type: 'array', items: { type: 'string' } },
|
|
66
|
+
ignoreUsagesIn: { type: 'array', items: { type: 'string' } },
|
|
67
|
+
reportPrivateCandidates: { type: 'boolean' },
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
create(context) {
|
|
74
|
+
const raw = (context.options?.[0] ?? {});
|
|
75
|
+
const ignoreNames = raw.ignoreNames ?? DEFAULT_IGNORE_NAMES;
|
|
76
|
+
const ignoreUsagesIn = raw.ignoreUsagesIn ?? DEFAULT_IGNORE_USAGES_IN;
|
|
77
|
+
const reportPrivateCandidates = raw.reportPrivateCandidates ?? false;
|
|
78
|
+
const index = getProjectIndex(context.cwd, readProjectSettings(context));
|
|
79
|
+
const self = context.filename;
|
|
80
|
+
// A file outside the indexed roots (or an unsaved buffer) has no entry, and every member in it
|
|
81
|
+
// would look unused. Staying silent is the only safe answer.
|
|
82
|
+
if (!index.files.has(self)) {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
const countsAsUsage = (path) => path !== self && !matchesGlob(relative(index.root, path).split('\\').join('/'), ignoreUsagesIn);
|
|
86
|
+
return {
|
|
87
|
+
ClassBody(node) {
|
|
88
|
+
const declaration = node.parent;
|
|
89
|
+
if (!declaration || isPolymorphic(declaration)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
for (const member of (node.body ?? [])) {
|
|
93
|
+
if (!isReportableMember(member, ignoreNames)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const name = member.key.name;
|
|
97
|
+
const seenIn = index.memberUsage.get(name);
|
|
98
|
+
if (seenIn && [...seenIn].some(countsAsUsage)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const usedInternally = Boolean(seenIn?.has(self));
|
|
102
|
+
if (usedInternally && !reportPrivateCandidates) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
context.report({
|
|
106
|
+
node: member.key,
|
|
107
|
+
messageId: usedInternally ? 'privateCandidate' : 'unused',
|
|
108
|
+
data: { name, file: relative(index.root, self) },
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
export default rule;
|
|
116
|
+
//# sourceMappingURL=no-unused-class-members.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unused-class-members.mjs","sourceRoot":"","sources":["../../src/rules/no-unused-class-members.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAoD,MAAM,cAAc,CAAC;AAEhF;;;;;;;;;;GAUG;AAEH,MAAM,oBAAoB,GAAG;IAC3B,aAAa;IACb,yEAAyE;IACzE,cAAc;IACd,iBAAiB;IACjB,wBAAwB;IACxB,uBAAuB;IACvB,2BAA2B;CAC5B,CAAC;AAEF,iFAAiF;AACjF,MAAM,wBAAwB,GAAG,CAAC,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;AAYjG,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,kBAAkB,EAAE,oBAAoB,EAAE,kBAAkB,CAAC,CAAC,CAAC;AAE7F;;;;;GAKG;AACH,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,IAAK,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAY,CAAC,MAAM,GAAG,CAAC,CAAC;AAEjH,MAAM,kBAAkB,GAAG,CAAC,MAAY,EAAE,WAAqB,EAAE,EAAE;IACjE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,QAAQ,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,KAAK,YAAY,EAAE,CAAC;QAC3F,OAAO,KAAK,CAAC;IACf,CAAC;IAED,2FAA2F;IAC3F,IAAI,MAAM,CAAC,aAAa,KAAK,SAAS,IAAI,MAAM,CAAC,IAAI,KAAK,aAAa,EAAE,CAAC;QACxE,OAAO,KAAK,CAAC;IACf,CAAC;IAED,gGAAgG;IAChG,gGAAgG;IAChG,kDAAkD;IAClD,IAAK,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,WAAW,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,IAAc,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAS;IACjB,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,kFAAkF;SAChG;QACD,QAAQ,EAAE;YACR,MAAM,EAAE,2EAA2E;YACnF,gBAAgB,EAAE,mEAAmE;SACtF;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,WAAW,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;oBACzD,cAAc,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;oBAC5D,uBAAuB,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;iBAC7C;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,MAAM,CAAC,OAAgB;QACrB,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAqB,CAAC;QAE7D,MAAM,WAAW,GAAG,GAAG,CAAC,WAAW,IAAI,oBAAoB,CAAC;QAC5D,MAAM,cAAc,GAAG,GAAG,CAAC,cAAc,IAAI,wBAAwB,CAAC;QACtE,MAAM,uBAAuB,GAAG,GAAG,CAAC,uBAAuB,IAAI,KAAK,CAAC;QAErE,MAAM,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC;QACzE,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC;QAE9B,+FAA+F;QAC/F,6DAA6D;QAC7D,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,EAAE,CACrC,IAAI,KAAK,IAAI,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,cAAc,CAAC,CAAC;QAElG,OAAO;YACL,SAAS,CAAC,IAAU;gBAClB,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC;gBAEhC,IAAI,CAAC,WAAW,IAAI,aAAa,CAAC,WAAW,CAAC,EAAE,CAAC;oBAC/C,OAAO;gBACT,CAAC;gBAED,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAW,EAAE,CAAC;oBACjD,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC;wBAC7C,SAAS;oBACX,CAAC;oBAED,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,IAAc,CAAC;oBACvC,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBAE3C,IAAI,MAAM,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC;wBAC9C,SAAS;oBACX,CAAC;oBAED,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;oBAElD,IAAI,cAAc,IAAI,CAAC,uBAAuB,EAAE,CAAC;wBAC/C,SAAS;oBACX,CAAC;oBAED,OAAO,CAAC,MAAM,CAAC;wBACb,IAAI,EAAE,MAAM,CAAC,GAAG;wBAChB,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,QAAQ;wBACzD,IAAI,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE;qBACjD,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
* Port of `import/no-unused-modules` from `eslint-plugin-import`, which this repo used before
|
|
7
|
+
* moving to oxlint.
|
|
8
|
+
*
|
|
9
|
+
* It overlaps with `knip --production` by design — the difference is where the diagnostic lands.
|
|
10
|
+
* knip prints a report at the end of a separate command; this puts the error on the export itself,
|
|
11
|
+
* in the editor, on the same pass as every other lint rule.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Always applied, on top of whatever a project configures. A `*.config.*` file is loaded by path by
|
|
15
|
+
* the tool that owns it, so its default export is an entry point by definition, never an import.
|
|
16
|
+
*/
|
|
17
|
+
const ALWAYS_IGNORED_EXPORTS = ['**/*.config.*'];
|
|
18
|
+
const rule = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: 'problem',
|
|
21
|
+
docs: {
|
|
22
|
+
description: 'Disallow modules without exports, and exports nothing else imports',
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
unused: "'{{name}}' is exported but never imported.",
|
|
26
|
+
missing: 'This file has no exports.',
|
|
27
|
+
},
|
|
28
|
+
schema: [
|
|
29
|
+
{
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
unusedExports: { type: 'boolean' },
|
|
33
|
+
missingExports: { type: 'boolean' },
|
|
34
|
+
ignoreExports: { type: 'array', items: { type: 'string' } },
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const raw = (context.options?.[0] ?? {});
|
|
42
|
+
const unusedExports = raw.unusedExports ?? true;
|
|
43
|
+
const missingExports = raw.missingExports ?? false;
|
|
44
|
+
const ignoreExports = [...ALWAYS_IGNORED_EXPORTS, ...(raw.ignoreExports ?? [])];
|
|
45
|
+
const index = getProjectIndex(context.cwd, readProjectSettings(context));
|
|
46
|
+
const self = context.filename;
|
|
47
|
+
const file = index.files.get(self);
|
|
48
|
+
if (!file) {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
const relativePath = relative(index.root, self).split('\\').join('/');
|
|
52
|
+
if (matchesGlob(relativePath, ignoreExports)) {
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
const importers = index.importedFrom.get(self);
|
|
56
|
+
// A namespace import (`import * as x`) or a bare `import './x'` re-export pulls in everything,
|
|
57
|
+
// so no individual export can be called unused.
|
|
58
|
+
const wholeModuleUsed = Boolean(importers?.has('*'));
|
|
59
|
+
const isUsed = (name) => {
|
|
60
|
+
if (wholeModuleUsed || importers?.has(name)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// `@public` is the escape hatch knip already honours, and the codebase already uses it.
|
|
64
|
+
if (file.intentionalExports.has(name)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
// An export consumed inside its own file is over-exposed, not dead — the fix is dropping the
|
|
68
|
+
// `export` keyword, not deleting the declaration. Reporting it as unused would be wrong, and
|
|
69
|
+
// it is what most of a codebase's exported return types look like.
|
|
70
|
+
return file.localReferences.has(name);
|
|
71
|
+
};
|
|
72
|
+
const report = (node, name) => {
|
|
73
|
+
if (!isUsed(name)) {
|
|
74
|
+
context.report({ node, messageId: 'unused', data: { name } });
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
return {
|
|
78
|
+
ExportNamedDeclaration(node) {
|
|
79
|
+
if (!unusedExports || node.source) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
for (const specifier of (node.specifiers ?? [])) {
|
|
83
|
+
if (specifier.exported?.name) {
|
|
84
|
+
report(specifier.exported, specifier.exported.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const declaration = node.declaration;
|
|
88
|
+
if (!declaration) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (declaration.id?.name) {
|
|
92
|
+
report(declaration.id, declaration.id.name);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const declarator of (declaration.declarations ?? [])) {
|
|
96
|
+
if (declarator.id?.type === 'Identifier') {
|
|
97
|
+
report(declarator.id, declarator.id.name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
ExportDefaultDeclaration(node) {
|
|
102
|
+
if (unusedExports) {
|
|
103
|
+
report(node, 'default');
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
'Program:exit'(node) {
|
|
107
|
+
if (missingExports && file.exports.size === 0) {
|
|
108
|
+
context.report({ node, messageId: 'missing' });
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
export default rule;
|
|
115
|
+
//# sourceMappingURL=no-unused-modules.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unused-modules.mjs","sourceRoot":"","sources":["../../src/rules/no-unused-modules.mts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {} from "../types.mjs";
|
|
2
|
+
/** Special marker meaning "every style on this sheet is used". */
|
|
3
|
+
const ALL_STYLES = '*';
|
|
4
|
+
const getStyleNames = (object) => object.properties
|
|
5
|
+
.filter(prop => prop.type === 'Property' && prop.key?.type === 'Identifier')
|
|
6
|
+
.map(prop => prop.key.name);
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the object literal a `StyleSheet.create` argument ultimately produces.
|
|
9
|
+
*
|
|
10
|
+
* Handles the plain object form, plus the two function forms react-native-unistyles uses:
|
|
11
|
+
* `create(theme => ({ ... }))` and `create(theme => { return { ... } })`.
|
|
12
|
+
*/
|
|
13
|
+
const getStylesObject = (argument) => {
|
|
14
|
+
if (!argument) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (argument.type === 'ObjectExpression') {
|
|
18
|
+
return argument;
|
|
19
|
+
}
|
|
20
|
+
if (argument.type !== 'ArrowFunctionExpression') {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (argument.body.type === 'ObjectExpression') {
|
|
24
|
+
return argument.body;
|
|
25
|
+
}
|
|
26
|
+
if (argument.body.type === 'BlockStatement') {
|
|
27
|
+
const returnStatement = argument.body.body.find(statement => statement.type === 'ReturnStatement');
|
|
28
|
+
if (returnStatement?.argument?.type === 'ObjectExpression') {
|
|
29
|
+
return returnStatement.argument;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
const rule = {
|
|
35
|
+
meta: {
|
|
36
|
+
type: 'problem',
|
|
37
|
+
docs: {
|
|
38
|
+
description: 'Disallow unused StyleSheet styles in React Native and react-native-unistyles',
|
|
39
|
+
},
|
|
40
|
+
messages: {
|
|
41
|
+
unusedStyle: 'Style "{{name}}" is defined but never used.',
|
|
42
|
+
},
|
|
43
|
+
schema: [],
|
|
44
|
+
fixable: 'code',
|
|
45
|
+
},
|
|
46
|
+
create(context) {
|
|
47
|
+
const styleSheets = new Map();
|
|
48
|
+
const usages = [];
|
|
49
|
+
return {
|
|
50
|
+
// Collect every member expression up front — the stylesheet it refers to may not have been
|
|
51
|
+
// visited yet, so matching is deferred to `Program:exit`.
|
|
52
|
+
MemberExpression(node) {
|
|
53
|
+
if (node.object.type !== 'Identifier') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let styleName = null;
|
|
57
|
+
if (node.property.type === 'Identifier' && !node.computed) {
|
|
58
|
+
styleName = node.property.name;
|
|
59
|
+
}
|
|
60
|
+
else if (node.property.type === 'Literal') {
|
|
61
|
+
styleName = node.property.value;
|
|
62
|
+
}
|
|
63
|
+
if (styleName) {
|
|
64
|
+
usages.push({ objectName: node.object.name, styleName });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
'CallExpression[callee.object.name="StyleSheet"][callee.property.name="create"]'(node) {
|
|
68
|
+
if (node.parent.type !== 'VariableDeclarator') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const variableName = node.parent.id.name;
|
|
72
|
+
const stylesNode = getStylesObject(node.arguments[0]);
|
|
73
|
+
if (!variableName || !stylesNode) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const styles = getStyleNames(stylesNode);
|
|
77
|
+
if (styles.length > 0) {
|
|
78
|
+
styleSheets.set(variableName, { node: stylesNode, styles: new Set(styles) });
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
// `const { container } = styles`
|
|
82
|
+
VariableDeclarator(node) {
|
|
83
|
+
if (node.init?.type !== 'Identifier' || node.id.type !== 'ObjectPattern') {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const objectName = node.init.name;
|
|
87
|
+
for (const prop of node.id.properties) {
|
|
88
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
89
|
+
usages.push({ objectName, styleName: prop.key.name });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// `{ ...styles }` — treat the whole sheet as used.
|
|
94
|
+
SpreadElement(node) {
|
|
95
|
+
if (node.argument?.type === 'Identifier') {
|
|
96
|
+
usages.push({ objectName: node.argument.name, styleName: ALL_STYLES });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
'Program:exit'() {
|
|
100
|
+
const usedStyles = new Set();
|
|
101
|
+
for (const { objectName, styleName } of usages) {
|
|
102
|
+
const sheet = styleSheets.get(objectName);
|
|
103
|
+
if (!sheet) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (styleName === ALL_STYLES) {
|
|
107
|
+
for (const name of sheet.styles) {
|
|
108
|
+
usedStyles.add(`${objectName}.${name}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
usedStyles.add(`${objectName}.${styleName}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const [sheetName, sheet] of styleSheets) {
|
|
116
|
+
for (const styleName of sheet.styles) {
|
|
117
|
+
if (usedStyles.has(`${sheetName}.${styleName}`)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const styleNode = sheet.node.properties.find(prop => prop.key?.name === styleName);
|
|
121
|
+
if (!styleNode) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
context.report({
|
|
125
|
+
node: styleNode.key,
|
|
126
|
+
messageId: 'unusedStyle',
|
|
127
|
+
data: { name: styleName },
|
|
128
|
+
fix(fixer) {
|
|
129
|
+
const { sourceCode } = context;
|
|
130
|
+
const tokenAfter = sourceCode.getTokenAfter(styleNode);
|
|
131
|
+
const tokenBefore = sourceCode.getTokenBefore(styleNode);
|
|
132
|
+
// Swallow the trailing comma, or the leading one for the last property.
|
|
133
|
+
if (tokenAfter?.value === ',') {
|
|
134
|
+
return fixer.removeRange([styleNode.range[0], tokenAfter.range[1]]);
|
|
135
|
+
}
|
|
136
|
+
if (tokenBefore?.value === ',') {
|
|
137
|
+
return fixer.removeRange([tokenBefore.range[0], styleNode.range[1]]);
|
|
138
|
+
}
|
|
139
|
+
return fixer.remove(styleNode);
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
export default rule;
|
|
149
|
+
//# sourceMappingURL=no-unused-styles.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"no-unused-styles.mjs","sourceRoot":"","sources":["../../src/rules/no-unused-styles.mts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {} from "../types.mjs";
|
|
2
|
+
const SCALAR_TYPES = ['ID', 'String', 'Boolean', 'Int', 'Float', 'Number', 'Date'];
|
|
3
|
+
const CACHE_FIELD_IMPORT = "import { CacheField } from '@/libs/cache-control/cache-field.decorator';";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the GraphQL type name a `@Field(() => X)` thunk returns.
|
|
6
|
+
*
|
|
7
|
+
* Handles the two forms the codebase uses: `() => Type` and `() => [Type]`. Anything else
|
|
8
|
+
* (a member expression, a call, a literal) is treated as unknown and left alone.
|
|
9
|
+
*/
|
|
10
|
+
const getReturnedTypeName = (body) => {
|
|
11
|
+
if (body.type === 'Identifier') {
|
|
12
|
+
return body.name;
|
|
13
|
+
}
|
|
14
|
+
if (body.type === 'ArrayExpression') {
|
|
15
|
+
const first = body.elements[0];
|
|
16
|
+
if (first?.type === 'Identifier') {
|
|
17
|
+
return first.name;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* We only want `@CacheField` on classes declared as `@ObjectType()`, because those are the
|
|
24
|
+
* responses Apollo caches. `@InputType()` is ignored — we don't cache mutation inputs.
|
|
25
|
+
*/
|
|
26
|
+
const isObjectTypeMember = (property) => {
|
|
27
|
+
const classNode = property.parent?.parent;
|
|
28
|
+
return (classNode?.decorators ?? []).some(decorator => decorator.expression?.callee?.name === 'ObjectType');
|
|
29
|
+
};
|
|
30
|
+
const rule = {
|
|
31
|
+
meta: {
|
|
32
|
+
type: 'problem',
|
|
33
|
+
docs: {
|
|
34
|
+
description: 'Require `@CacheField` over `@Field` for non-scalar GraphQL object type fields',
|
|
35
|
+
},
|
|
36
|
+
messages: {
|
|
37
|
+
noCacheControl: `Fields resolving as arrays/objects should configure @CacheField to not break Apollo's caching logic.`,
|
|
38
|
+
},
|
|
39
|
+
schema: [],
|
|
40
|
+
hasSuggestions: true,
|
|
41
|
+
},
|
|
42
|
+
create(context) {
|
|
43
|
+
let hasImport = false;
|
|
44
|
+
let lastImport = null;
|
|
45
|
+
return {
|
|
46
|
+
ImportDeclaration(node) {
|
|
47
|
+
lastImport = node;
|
|
48
|
+
for (const specifier of (node.specifiers ?? [])) {
|
|
49
|
+
if (specifier.type === 'ImportSpecifier' && specifier.local?.name === 'CacheField') {
|
|
50
|
+
hasImport = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
PropertyDefinition(node) {
|
|
55
|
+
if (!isObjectTypeMember(node)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const decorators = (node.decorators ?? []);
|
|
59
|
+
const hasCacheControl = decorators.some(decorator => decorator.expression?.callee?.name === 'CacheControl');
|
|
60
|
+
if (hasCacheControl) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const decorator of decorators) {
|
|
64
|
+
const callExpression = decorator.expression;
|
|
65
|
+
if (callExpression?.type !== 'CallExpression' || callExpression.callee?.name !== 'Field') {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const thunk = callExpression.arguments[0];
|
|
69
|
+
if (thunk?.type !== 'ArrowFunctionExpression') {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const typeName = getReturnedTypeName(thunk.body);
|
|
73
|
+
if (typeName === null || SCALAR_TYPES.includes(typeName) || /json/i.test(typeName)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
context.report({
|
|
77
|
+
node: callExpression,
|
|
78
|
+
messageId: 'noCacheControl',
|
|
79
|
+
suggest: [
|
|
80
|
+
{
|
|
81
|
+
desc: 'Use @CacheField() instead.',
|
|
82
|
+
fix: fixer => {
|
|
83
|
+
const fixes = [fixer.replaceText(callExpression.callee, 'CacheField')];
|
|
84
|
+
if (!hasImport) {
|
|
85
|
+
if (lastImport) {
|
|
86
|
+
// insert after the last import decl
|
|
87
|
+
fixes.push(fixer.insertTextAfter(lastImport, `\n${CACHE_FIELD_IMPORT}`));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// insert at the start of the file
|
|
91
|
+
fixes.push(fixer.insertTextAfterRange([0, 0], `${CACHE_FIELD_IMPORT}\n`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return fixes;
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
export default rule;
|
|
105
|
+
//# sourceMappingURL=require-cache-control.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"require-cache-control.mjs","sourceRoot":"","sources":["../../src/rules/require-cache-control.mts"],"names":[],"mappings":"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"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural types for oxlint's JS plugin API.
|
|
3
|
+
*
|
|
4
|
+
* Oxlint ships the full definitions in `oxlint/plugins-dev`, but only `RuleTester` is actually
|
|
5
|
+
* exported from that entrypoint, so the rule/context types are not importable. These cover the
|
|
6
|
+
* surface the local rules use — the AST is ESTree-shaped, same as ESLint's.
|
|
7
|
+
*/
|
|
8
|
+
export interface Node {
|
|
9
|
+
type: string;
|
|
10
|
+
parent: Node;
|
|
11
|
+
range: [number, number];
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
interface Token {
|
|
15
|
+
type: string;
|
|
16
|
+
value: string;
|
|
17
|
+
range: [number, number];
|
|
18
|
+
}
|
|
19
|
+
interface Fixer {
|
|
20
|
+
remove(node: Node): unknown;
|
|
21
|
+
removeRange(range: [number, number]): unknown;
|
|
22
|
+
replaceText(node: Node, text: string): unknown;
|
|
23
|
+
insertTextAfter(node: Node, text: string): unknown;
|
|
24
|
+
insertTextAfterRange(range: [number, number], text: string): unknown;
|
|
25
|
+
}
|
|
26
|
+
interface Suggestion {
|
|
27
|
+
desc: string;
|
|
28
|
+
fix: (fixer: Fixer) => unknown;
|
|
29
|
+
}
|
|
30
|
+
interface SourceCode {
|
|
31
|
+
getTokenAfter(node: Node): Token | null;
|
|
32
|
+
getTokenBefore(node: Node): Token | null;
|
|
33
|
+
}
|
|
34
|
+
export interface Context {
|
|
35
|
+
sourceCode: SourceCode;
|
|
36
|
+
/** Absolute path of the file being linted. */
|
|
37
|
+
filename: string;
|
|
38
|
+
/** Absolute path oxlint was invoked from — the project root. */
|
|
39
|
+
cwd: string;
|
|
40
|
+
/** Whatever was passed after the severity in `oxlint.config.mts`. */
|
|
41
|
+
options?: unknown[];
|
|
42
|
+
/** The config's `settings` block — shared by rules, unlike per-rule `options`. */
|
|
43
|
+
settings?: Record<string, unknown>;
|
|
44
|
+
report(diagnostic: {
|
|
45
|
+
node: Node;
|
|
46
|
+
messageId: string;
|
|
47
|
+
data?: Record<string, string | number | boolean | null | undefined>;
|
|
48
|
+
fix?: (fixer: Fixer) => unknown;
|
|
49
|
+
suggest?: Suggestion[];
|
|
50
|
+
}): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Named so a rule that bails early can annotate its `create` return and have `return {}` checked
|
|
54
|
+
* against this directly, instead of TS widening the two branches into an awkward union.
|
|
55
|
+
*/
|
|
56
|
+
export type Visitor = Record<string, (node: any) => void>;
|
|
57
|
+
export interface Rule {
|
|
58
|
+
meta: {
|
|
59
|
+
type: 'problem' | 'suggestion' | 'layout';
|
|
60
|
+
docs: {
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
messages: Record<string, string>;
|
|
64
|
+
schema: unknown[];
|
|
65
|
+
fixable?: 'code' | 'whitespace';
|
|
66
|
+
hasSuggestions?: boolean;
|
|
67
|
+
};
|
|
68
|
+
create(context: Context): Visitor;
|
|
69
|
+
}
|
|
70
|
+
export {};
|
package/dist/types.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal structural types for oxlint's JS plugin API.
|
|
3
|
+
*
|
|
4
|
+
* Oxlint ships the full definitions in `oxlint/plugins-dev`, but only `RuleTester` is actually
|
|
5
|
+
* exported from that entrypoint, so the rule/context types are not importable. These cover the
|
|
6
|
+
* surface the local rules use — the AST is ESTree-shaped, same as ESLint's.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.mjs","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|