ai-deslop 0.2.0 → 0.2.2

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.
Files changed (97) hide show
  1. package/README.md +35 -106
  2. package/dist/index.d.ts +18 -0
  3. package/dist/index.js +84 -0
  4. package/dist/rules/no-array-filter-map.d.ts +2 -0
  5. package/dist/rules/no-array-filter-map.js +30 -0
  6. package/dist/rules/no-call-only-assertions.d.ts +2 -0
  7. package/dist/rules/no-call-only-assertions.js +87 -0
  8. package/dist/rules/no-chained-type-assertions.d.ts +2 -0
  9. package/dist/rules/no-chained-type-assertions.js +60 -0
  10. package/dist/rules/no-conditional-empty-object-spread.d.ts +2 -0
  11. package/dist/rules/no-conditional-empty-object-spread.js +40 -0
  12. package/dist/rules/no-duplicate-types.d.ts +2 -0
  13. package/dist/rules/no-duplicate-types.js +143 -0
  14. package/dist/rules/no-known-value-widening.d.ts +2 -0
  15. package/dist/rules/no-known-value-widening.js +300 -0
  16. package/dist/rules/no-module-mocking.d.ts +2 -0
  17. package/dist/rules/no-module-mocking.js +69 -0
  18. package/dist/rules/no-object-parameters.d.ts +2 -0
  19. package/dist/rules/no-object-parameters.js +56 -0
  20. package/dist/rules/no-pass-through-type-alias.d.ts +2 -0
  21. package/dist/rules/no-pass-through-type-alias.js +41 -0
  22. package/dist/rules/no-reduce-accumulator-copy.d.ts +2 -0
  23. package/dist/rules/no-reduce-accumulator-copy.js +99 -0
  24. package/dist/rules/no-reflect-apply.d.ts +2 -0
  25. package/dist/rules/no-reflect-apply.js +25 -0
  26. package/dist/rules/no-reflect-get.d.ts +2 -0
  27. package/dist/rules/no-reflect-get.js +25 -0
  28. package/dist/rules/no-runtime-typeof.d.ts +2 -0
  29. package/dist/rules/no-runtime-typeof.js +64 -0
  30. package/dist/rules/no-shape-in-symbol-names.d.ts +2 -0
  31. package/dist/rules/no-shape-in-symbol-names.js +40 -0
  32. package/dist/rules/no-unknown-parameters.d.ts +2 -0
  33. package/dist/rules/no-unknown-parameters.js +51 -0
  34. package/dist/rules/no-unknown-returns.d.ts +2 -0
  35. package/dist/rules/no-unknown-returns.js +58 -0
  36. package/dist/rules/no-unknown-type-aliases.d.ts +2 -0
  37. package/dist/rules/no-unknown-type-aliases.js +40 -0
  38. package/dist/rules/no-unsafe-dictionary-type.d.ts +2 -0
  39. package/dist/rules/no-unsafe-dictionary-type.js +134 -0
  40. package/dist/rules/no-widen-then-assert.d.ts +2 -0
  41. package/dist/rules/no-widen-then-assert.js +270 -0
  42. package/dist/rules/require-safety-comment-for-type-assertion.d.ts +2 -0
  43. package/dist/rules/require-safety-comment-for-type-assertion.js +100 -0
  44. package/dist/shared/array-method.d.ts +12 -0
  45. package/dist/shared/array-method.js +84 -0
  46. package/dist/shared/dictionary-types.d.ts +20 -0
  47. package/dist/shared/dictionary-types.js +377 -0
  48. package/dist/shared/function-parameters.d.ts +8 -0
  49. package/dist/shared/function-parameters.js +40 -0
  50. package/dist/shared/lexical-type-parameters.d.ts +5 -0
  51. package/dist/shared/lexical-type-parameters.js +47 -0
  52. package/dist/shared/reflect-method.d.ts +3 -0
  53. package/dist/shared/reflect-method.js +20 -0
  54. package/dist/shared/scope.d.ts +3 -0
  55. package/dist/shared/scope.js +11 -0
  56. package/dist/shared/type-alias-resolution.d.ts +23 -0
  57. package/dist/shared/type-alias-resolution.js +162 -0
  58. package/dist/tailwind/candidates.d.ts +14 -0
  59. package/dist/tailwind/candidates.js +24 -0
  60. package/dist/tailwind/design-system.d.ts +3 -0
  61. package/dist/tailwind/design-system.js +41 -0
  62. package/dist/tailwind/rule.d.ts +3 -0
  63. package/dist/tailwind/rule.js +133 -0
  64. package/dist/tailwind/worker.d.ts +1 -0
  65. package/dist/tailwind/worker.js +13 -0
  66. package/package.json +16 -5
  67. package/src/index.ts +0 -87
  68. package/src/rules/no-array-filter-map.ts +0 -28
  69. package/src/rules/no-call-only-assertions.ts +0 -94
  70. package/src/rules/no-chained-type-assertions.ts +0 -77
  71. package/src/rules/no-conditional-empty-object-spread.ts +0 -49
  72. package/src/rules/no-known-value-widening.ts +0 -415
  73. package/src/rules/no-module-mocking.ts +0 -80
  74. package/src/rules/no-object-parameters.ts +0 -83
  75. package/src/rules/no-pass-through-type-alias.ts +0 -52
  76. package/src/rules/no-reduce-accumulator-copy.ts +0 -109
  77. package/src/rules/no-reflect-apply.ts +0 -28
  78. package/src/rules/no-reflect-get.ts +0 -28
  79. package/src/rules/no-runtime-typeof.ts +0 -77
  80. package/src/rules/no-shape-in-symbol-names.ts +0 -46
  81. package/src/rules/no-unknown-parameters.ts +0 -69
  82. package/src/rules/no-unknown-returns.ts +0 -82
  83. package/src/rules/no-unknown-type-aliases.ts +0 -54
  84. package/src/rules/no-unsafe-dictionary-type.ts +0 -154
  85. package/src/rules/no-widen-then-assert.ts +0 -366
  86. package/src/rules/require-safety-comment-for-type-assertion.ts +0 -131
  87. package/src/shared/array-method.ts +0 -94
  88. package/src/shared/dictionary-types.ts +0 -515
  89. package/src/shared/function-parameters.ts +0 -49
  90. package/src/shared/lexical-type-parameters.ts +0 -61
  91. package/src/shared/reflect-method.ts +0 -24
  92. package/src/shared/scope.ts +0 -15
  93. package/src/shared/type-alias-resolution.ts +0 -250
  94. package/src/tailwind/candidates.ts +0 -38
  95. package/src/tailwind/design-system.ts +0 -59
  96. package/src/tailwind/rule.ts +0 -126
  97. package/src/tailwind/worker.ts +0 -38
@@ -0,0 +1,162 @@
1
+ import { lexicalTypeParameterNames } from "./lexical-type-parameters.js";
2
+ const environmentsByProgram = new WeakMap();
3
+ function isNode(value) {
4
+ return (typeof value === "object" &&
5
+ value !== null &&
6
+ "type" in value &&
7
+ typeof value.type === "string");
8
+ }
9
+ function enclosingTypeScope(node) {
10
+ let current = node.parent;
11
+ while (current !== null) {
12
+ if (current.type === "Program" ||
13
+ current.type === "BlockStatement" ||
14
+ current.type === "TSModuleBlock" ||
15
+ current.type === "StaticBlock" ||
16
+ current.type === "SwitchStatement") {
17
+ return current;
18
+ }
19
+ current = current.parent;
20
+ }
21
+ return node;
22
+ }
23
+ function declaredTypeBinding(node) {
24
+ if (node.type === "TSTypeAliasDeclaration") {
25
+ return { alias: node, name: node.id.name };
26
+ }
27
+ if (node.type === "TSInterfaceDeclaration" ||
28
+ node.type === "TSEnumDeclaration" ||
29
+ node.type === "ClassDeclaration" ||
30
+ node.type === "ClassExpression") {
31
+ return node.id === null ? null : { alias: null, name: node.id.name };
32
+ }
33
+ if (node.type === "ImportSpecifier" ||
34
+ node.type === "ImportDefaultSpecifier" ||
35
+ node.type === "ImportNamespaceSpecifier") {
36
+ return { alias: null, name: node.local.name };
37
+ }
38
+ return null;
39
+ }
40
+ function collectTypeBindings(node, visitorKeys, bindingsByName, aliases) {
41
+ const declared = declaredTypeBinding(node);
42
+ if (declared !== null) {
43
+ const bindings = bindingsByName.get(declared.name) ?? [];
44
+ bindings.push({ ...declared, scope: enclosingTypeScope(node) });
45
+ bindingsByName.set(declared.name, bindings);
46
+ if (declared.alias !== null)
47
+ aliases.push(declared.alias);
48
+ }
49
+ // SAFETY: Oxlint's visitor keys identify only ESTree child-node properties.
50
+ const fields = node;
51
+ for (const key of visitorKeys[node.type] ?? []) {
52
+ const value = fields[key];
53
+ if (isNode(value)) {
54
+ collectTypeBindings(value, visitorKeys, bindingsByName, aliases);
55
+ continue;
56
+ }
57
+ if (!Array.isArray(value))
58
+ continue;
59
+ for (const child of value) {
60
+ if (isNode(child)) {
61
+ collectTypeBindings(child, visitorKeys, bindingsByName, aliases);
62
+ }
63
+ }
64
+ }
65
+ }
66
+ /** Collect every lexical type alias and competing type binding in a program. */
67
+ export function createTypeAliasEnvironment(program, visitorKeys) {
68
+ const cached = environmentsByProgram.get(program);
69
+ if (cached !== undefined)
70
+ return cached;
71
+ const bindingsByName = new Map();
72
+ const aliases = [];
73
+ collectTypeBindings(program, visitorKeys, bindingsByName, aliases);
74
+ const environment = { aliases, bindingsByName, visitorKeys };
75
+ environmentsByProgram.set(program, environment);
76
+ return environment;
77
+ }
78
+ function ancestorDistance(ancestor, node) {
79
+ let current = node;
80
+ let distance = 0;
81
+ while (current !== null) {
82
+ if (current === ancestor)
83
+ return distance;
84
+ current = current.parent;
85
+ distance += 1;
86
+ }
87
+ return null;
88
+ }
89
+ function nearestTypeBindings(name, use, environment) {
90
+ const candidates = environment.bindingsByName.get(name) ?? [];
91
+ let nearestDistance = Number.POSITIVE_INFINITY;
92
+ let nearest = [];
93
+ for (const candidate of candidates) {
94
+ const distance = ancestorDistance(candidate.scope, use);
95
+ if (distance === null || distance > nearestDistance)
96
+ continue;
97
+ if (distance === nearestDistance) {
98
+ nearest.push(candidate);
99
+ continue;
100
+ }
101
+ nearestDistance = distance;
102
+ nearest = [candidate];
103
+ }
104
+ return nearest;
105
+ }
106
+ /** Resolve the nearest visible alias with this name, respecting lexical shadowing. */
107
+ export function visibleTypeAlias(name, use, environment) {
108
+ if (lexicalTypeParameterNames(use, environment.visitorKeys).has(name))
109
+ return null;
110
+ const bindings = nearestTypeBindings(name, use, environment);
111
+ return bindings.length === 1 ? (bindings[0]?.alias ?? null) : null;
112
+ }
113
+ /** Return whether a local declaration shadows a built-in type at this use. */
114
+ export function hasVisibleTypeBinding(name, use, environment) {
115
+ return (lexicalTypeParameterNames(use, environment.visitorKeys).has(name) ||
116
+ nearestTypeBindings(name, use, environment).length > 0);
117
+ }
118
+ function typeReferenceName(type) {
119
+ return type.typeName.type === "Identifier" ? type.typeName.name : null;
120
+ }
121
+ function aliasSubstitutions(alias, reference, base) {
122
+ const parameters = alias.typeParameters?.params ?? [];
123
+ const arguments_ = reference.typeArguments?.params ?? [];
124
+ const next = new Map(base);
125
+ for (const [index, parameter] of parameters.entries()) {
126
+ const explicitArgument = arguments_[index];
127
+ const argument = explicitArgument ?? parameter.default;
128
+ if (argument === null || argument === undefined)
129
+ return null;
130
+ const argumentSubstitutions = explicitArgument === undefined ? next : base;
131
+ next.set(parameter.name.name, {
132
+ type: argument,
133
+ substitutions: new Map(argumentSubstitutions),
134
+ });
135
+ }
136
+ return next;
137
+ }
138
+ /** Match a type after resolving visible aliases and substituting their type parameters. */
139
+ export function resolvedTypeMatches(type, environment, matcher) {
140
+ const evaluate = (current, substitutions, resolvingAliases) => {
141
+ if (current.type === "TSTypeReference") {
142
+ const name = typeReferenceName(current);
143
+ if (name !== null) {
144
+ const substitution = substitutions.get(name);
145
+ if (substitution !== undefined && !current.typeArguments?.params.length) {
146
+ return evaluate(substitution.type, substitution.substitutions, resolvingAliases);
147
+ }
148
+ const alias = visibleTypeAlias(name, current, environment);
149
+ if (alias !== null && !resolvingAliases.has(alias)) {
150
+ const nextSubstitutions = aliasSubstitutions(alias, current, substitutions);
151
+ if (nextSubstitutions !== null) {
152
+ const nextResolving = new Set(resolvingAliases);
153
+ nextResolving.add(alias);
154
+ return evaluate(alias.typeAnnotation, nextSubstitutions, nextResolving);
155
+ }
156
+ }
157
+ }
158
+ }
159
+ return matcher(current, (child) => evaluate(child, substitutions, resolvingAliases));
160
+ };
161
+ return evaluate(type, new Map(), new Set());
162
+ }
@@ -0,0 +1,14 @@
1
+ export interface SpellingChange {
2
+ original: string;
3
+ canonical: string;
4
+ }
5
+ export declare function rewriteClassValue(value: string, canonicalOf: ReadonlyMap<string, string>): {
6
+ fixed: string;
7
+ changes: Array<SpellingChange>;
8
+ };
9
+ export declare function uniqueTokens(value: string): Array<string>;
10
+ export declare function summarizeChanges(changes: ReadonlyArray<SpellingChange>): {
11
+ original: string;
12
+ canonical: string;
13
+ suffix: string;
14
+ };
@@ -0,0 +1,24 @@
1
+ export function rewriteClassValue(value, canonicalOf) {
2
+ const changes = [];
3
+ const fixed = value.replace(/\S+/g, (token) => {
4
+ const canonical = canonicalOf.get(token);
5
+ if (canonical !== undefined && canonical !== token) {
6
+ changes.push({ original: token, canonical });
7
+ return canonical;
8
+ }
9
+ return token;
10
+ });
11
+ return { fixed, changes };
12
+ }
13
+ export function uniqueTokens(value) {
14
+ const tokens = value.match(/\S+/g) ?? [];
15
+ return [...new Set(tokens)];
16
+ }
17
+ export function summarizeChanges(changes) {
18
+ const first = changes[0];
19
+ return {
20
+ original: first.original,
21
+ canonical: first.canonical,
22
+ suffix: changes.length > 1 ? ` (+${changes.length - 1} more in this string)` : "",
23
+ };
24
+ }
@@ -0,0 +1,3 @@
1
+ export declare function resolveCssPath(cssPath: string | undefined, cwd?: string): string;
2
+ export declare function ensureDesignSystem(cssFile: string): string;
3
+ export declare function canonicalizeTokens(key: string, tokens: Array<string>, rem: number): Map<string, string>;
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createSyncFn } from "synckit";
5
+ // Resolved relative to the compiled output: `tsc` emits `worker.ts` as
6
+ // `dist/tailwind/worker.js` next to this file, and consumers load the
7
+ // package from `dist`, where Node cannot type-strip `.ts` under node_modules.
8
+ const workerPath = fileURLToPath(new URL("./worker.js", import.meta.url));
9
+ let syncCall = null;
10
+ const loadedSystems = new Map();
11
+ function getSyncCall() {
12
+ if (syncCall === null) {
13
+ syncCall = createSyncFn(workerPath);
14
+ }
15
+ return syncCall;
16
+ }
17
+ export function resolveCssPath(cssPath, cwd = process.cwd()) {
18
+ if (!cssPath)
19
+ throw new Error("cssPath is required");
20
+ return path.isAbsolute(cssPath) ? cssPath : path.resolve(cwd, cssPath);
21
+ }
22
+ export function ensureDesignSystem(cssFile) {
23
+ const stat = fs.statSync(cssFile);
24
+ if (loadedSystems.get(cssFile) === stat.mtimeMs)
25
+ return cssFile;
26
+ const cssContent = fs.readFileSync(cssFile, "utf8");
27
+ getSyncCall()({ type: "load", key: cssFile, cssContent, base: path.dirname(cssFile) });
28
+ loadedSystems.set(cssFile, stat.mtimeMs);
29
+ return cssFile;
30
+ }
31
+ export function canonicalizeTokens(key, tokens, rem) {
32
+ if (tokens.length === 0)
33
+ return new Map();
34
+ const canonical = getSyncCall()({ type: "canonicalize", key, candidates: tokens, rem });
35
+ if (!Array.isArray(canonical))
36
+ throw new Error("Tailwind canonicalization returned an invalid result");
37
+ const result = new Map();
38
+ for (let i = 0; i < tokens.length; i++)
39
+ result.set(tokens[i], canonical[i] ?? tokens[i]);
40
+ return result;
41
+ }
@@ -0,0 +1,3 @@
1
+ export declare const PLUGIN_NAME = "deslop";
2
+ export declare const RULE_ID = "canonical-class-names";
3
+ export declare const canonicalClassNames: import("@oxlint/plugins").Rule;
@@ -0,0 +1,133 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { rewriteClassValue, summarizeChanges, uniqueTokens } from "./candidates.js";
3
+ import { canonicalizeTokens, ensureDesignSystem, resolveCssPath } from "./design-system.js";
4
+ export const PLUGIN_NAME = "deslop";
5
+ export const RULE_ID = "canonical-class-names";
6
+ const DEFAULT_ATTRIBUTES = ["class", "className"];
7
+ const DEFAULT_CALLEE_FUNCTIONS = ["cn", "clsx", "cva", "twMerge", "tw", "classNames", "cx"];
8
+ const DEFAULT_ROOT_FONT_SIZE = 16;
9
+ const OPTIONS_SCHEMA = {
10
+ type: "object",
11
+ properties: {
12
+ cssPath: { type: "string", description: "Path to the Tailwind v4 entry CSS." },
13
+ rootFontSize: { type: "number", description: "Root font size in px used for rem normalization." },
14
+ attributes: { type: "array", items: { type: "string" }, description: "JSX class-list attributes." },
15
+ calleeFunctions: { type: "array", items: { type: "string" }, description: "Class-list helper functions." },
16
+ },
17
+ required: ["cssPath"],
18
+ additionalProperties: false,
19
+ };
20
+ export const canonicalClassNames = defineRule({
21
+ meta: {
22
+ type: "suggestion",
23
+ docs: { description: "Enforce canonical Tailwind CSS class spellings." },
24
+ fixable: "code",
25
+ messages: {
26
+ nonCanonical: "Tailwind class '{{original}}' can be written as '{{canonical}}'.{{suffix}}",
27
+ cssNotFound: "Could not load Tailwind CSS entry file: {{path}}",
28
+ },
29
+ schema: [OPTIONS_SCHEMA],
30
+ },
31
+ createOnce(context) {
32
+ let attributes = new Set(DEFAULT_ATTRIBUTES);
33
+ let callees = new Set(DEFAULT_CALLEE_FUNCTIONS);
34
+ let rem = DEFAULT_ROOT_FONT_SIZE;
35
+ let cssFile = "";
36
+ let designKey = null;
37
+ function initFile() {
38
+ const options = (context.options?.[0] ?? {});
39
+ attributes = new Set(options.attributes ?? DEFAULT_ATTRIBUTES);
40
+ callees = new Set(options.calleeFunctions ?? DEFAULT_CALLEE_FUNCTIONS);
41
+ rem = options.rootFontSize ?? DEFAULT_ROOT_FONT_SIZE;
42
+ try {
43
+ cssFile = resolveCssPath(options.cssPath);
44
+ }
45
+ catch {
46
+ cssFile = options.cssPath ?? "";
47
+ }
48
+ designKey = null;
49
+ }
50
+ function getDesignKey() {
51
+ if (designKey === null) {
52
+ try {
53
+ designKey = ensureDesignSystem(cssFile);
54
+ }
55
+ catch {
56
+ designKey = false;
57
+ }
58
+ }
59
+ return designKey === false ? null : designKey;
60
+ }
61
+ function checkStringNode(node, value) {
62
+ const tokens = uniqueTokens(value);
63
+ if (tokens.length === 0)
64
+ return;
65
+ const key = getDesignKey();
66
+ if (key === null) {
67
+ context.report({ node, messageId: "cssNotFound", data: { path: cssFile } });
68
+ return;
69
+ }
70
+ let canonicalOf;
71
+ try {
72
+ canonicalOf = canonicalizeTokens(key, tokens, rem);
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ const { fixed, changes } = rewriteClassValue(value, canonicalOf);
78
+ if (changes.length === 0)
79
+ return;
80
+ const summary = summarizeChanges(changes);
81
+ context.report({
82
+ node,
83
+ messageId: "nonCanonical",
84
+ data: summary,
85
+ fix(fixer) {
86
+ const first = context.sourceCode.getText(node)[0];
87
+ const quote = first === "'" || first === '"' || first === "`" ? first : '"';
88
+ return fixer.replaceText(node, `${quote}${fixed}${quote}`);
89
+ },
90
+ });
91
+ }
92
+ function checkExpression(node) {
93
+ if (node.type === "Literal" && typeof node.value === "string")
94
+ checkStringNode(node, node.value);
95
+ else if (node.type === "TemplateLiteral" && node.expressions.length === 0 && node.quasis.length === 1) {
96
+ checkStringNode(node, node.quasis[0].value.raw);
97
+ }
98
+ }
99
+ return {
100
+ before() {
101
+ initFile();
102
+ const text = context.sourceCode.text;
103
+ for (const name of attributes)
104
+ if (name !== "" && text.includes(name))
105
+ return;
106
+ for (const name of callees)
107
+ if (name !== "" && text.includes(name))
108
+ return;
109
+ return false;
110
+ },
111
+ JSXAttribute(node) {
112
+ if (node.name.type !== "JSXIdentifier" || !attributes.has(node.name.name) || !node.value)
113
+ return;
114
+ if (node.value.type === "Literal" && typeof node.value.value === "string")
115
+ checkStringNode(node.value, node.value.value);
116
+ else if (node.value.type === "JSXExpressionContainer")
117
+ checkExpression(node.value.expression);
118
+ },
119
+ CallExpression(node) {
120
+ let name = null;
121
+ if (node.callee.type === "Identifier")
122
+ name = node.callee.name;
123
+ else if (node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier")
124
+ name = node.callee.property.name;
125
+ if (name === null || !callees.has(name))
126
+ return;
127
+ for (const arg of node.arguments)
128
+ if (arg.type === "Literal" || arg.type === "TemplateLiteral")
129
+ checkExpression(arg);
130
+ },
131
+ };
132
+ },
133
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { __unstable__loadDesignSystem } from "@tailwindcss/node";
2
+ import { runAsWorker } from "synckit";
3
+ const designSystems = new Map();
4
+ runAsWorker(async (job) => {
5
+ if (job.type === "load") {
6
+ designSystems.set(job.key, await __unstable__loadDesignSystem(job.cssContent, { base: job.base }));
7
+ return { ok: true };
8
+ }
9
+ const designSystem = designSystems.get(job.key);
10
+ if (!designSystem)
11
+ throw new Error(`Tailwind design system not loaded for "${job.key}"`);
12
+ return designSystem.canonicalizeCandidates(job.candidates, { rem: job.rem });
13
+ });
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "ai-deslop",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
5
  "type": "module",
6
- "exports": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
7
14
  "files": [
8
- "src"
15
+ "dist"
9
16
  ],
10
17
  "scripts": {
11
- "check": "oxlint src && tsc --noEmit",
18
+ "build": "tsc -p tsconfig.build.json",
19
+ "prepublishOnly": "npm run build",
20
+ "check": "npm run build && oxlint src && tsc --noEmit",
12
21
  "lint": "oxlint src",
22
+ "test": "vitest run",
13
23
  "typecheck": "tsc --noEmit"
14
24
  },
15
25
  "dependencies": {
@@ -28,7 +38,8 @@
28
38
  "devDependencies": {
29
39
  "@types/node": "26.2.0",
30
40
  "oxlint": "1.82.0",
31
- "typescript": "7.0.2"
41
+ "typescript": "7.0.2",
42
+ "vitest": "^5.0.1"
32
43
  },
33
44
  "license": "MIT",
34
45
  "repository": {
package/src/index.ts DELETED
@@ -1,87 +0,0 @@
1
- import { eslintCompatPlugin } from "@oxlint/plugins";
2
-
3
- import { noArrayFilterMapRule } from "./rules/no-array-filter-map.ts";
4
- import { noReduceAccumulatorCopyRule } from "./rules/no-reduce-accumulator-copy.ts";
5
- import { noChainedTypeAssertionsRule } from "./rules/no-chained-type-assertions.ts";
6
- import { noConditionalEmptyObjectSpreadRule } from "./rules/no-conditional-empty-object-spread.ts";
7
- import { noKnownValueWideningRule } from "./rules/no-known-value-widening.ts";
8
- import { noModuleMockingRule } from "./rules/no-module-mocking.ts";
9
- import { noObjectParametersRule } from "./rules/no-object-parameters.ts";
10
- import { noReflectApplyRule } from "./rules/no-reflect-apply.ts";
11
- import { noReflectGetRule } from "./rules/no-reflect-get.ts";
12
- import { noRuntimeTypeofRule } from "./rules/no-runtime-typeof.ts";
13
- import { noForbiddenTermInSymbolNamesRule } from "./rules/no-shape-in-symbol-names.ts";
14
- import { noUnknownParametersRule } from "./rules/no-unknown-parameters.ts";
15
- import { noUnknownReturnsRule } from "./rules/no-unknown-returns.ts";
16
- import { noUnknownTypeAliasesRule } from "./rules/no-unknown-type-aliases.ts";
17
- import { noUnsafeDictionaryTypeRule } from "./rules/no-unsafe-dictionary-type.ts";
18
- import { noWidenThenAssertRule } from "./rules/no-widen-then-assert.ts";
19
- import { requireSafetyCommentForTypeAssertionRule } from "./rules/require-safety-comment-for-type-assertion.ts";
20
- import { noCallOnlyAssertionsRule } from "./rules/no-call-only-assertions.ts";
21
- import { noPassThroughTypeAliasRule } from "./rules/no-pass-through-type-alias.ts";
22
- import { canonicalClassNames } from "./tailwind/rule.ts";
23
-
24
- /** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
25
- export const recommendedRules: Record<string, "error"> = {
26
- "deslop/no-array-filter-map": "error",
27
- "deslop/no-reduce-accumulator-copy": "error",
28
- "deslop/no-chained-type-assertions": "error",
29
- "deslop/no-conditional-empty-object-spread": "error",
30
- "deslop/no-known-value-widening": "error",
31
- "deslop/no-module-mocking": "error",
32
- "deslop/no-object-parameters": "error",
33
- "deslop/no-reflect-apply": "error",
34
- "deslop/no-reflect-get": "error",
35
- "deslop/no-runtime-typeof": "error",
36
- "deslop/no-shape-in-symbol-names": "error",
37
- "deslop/no-unknown-parameters": "error",
38
- "deslop/no-unknown-returns": "error",
39
- "deslop/no-unknown-type-aliases": "error",
40
- "deslop/no-unsafe-dictionary-type": "error",
41
- "deslop/no-widen-then-assert": "error",
42
- "deslop/require-safety-comment-for-type-assertion": "error",
43
- "deslop/no-call-only-assertions": "error",
44
- "deslop/no-pass-through-type-alias": "error",
45
- "typescript/no-deprecated": "error",
46
- "typescript/no-floating-promises": "error",
47
- };
48
-
49
- /** ESLint flat-config style configs. `recommended` enables every `deslop/*` rule as `"error"` (except opt-in rules like `canonical-class-names`), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
50
- export const configs = {
51
- recommended: {
52
- name: "deslop/recommended",
53
- options: { typeAware: true },
54
- rules: recommendedRules,
55
- },
56
- };
57
-
58
- /** Generic Oxlint rules that reject low-evidence and low-signal implementation patterns. */
59
- const deslopPlugin = eslintCompatPlugin({
60
- meta: { name: "deslop" },
61
- rules: {
62
- "no-array-filter-map": noArrayFilterMapRule,
63
- "no-reduce-accumulator-copy": noReduceAccumulatorCopyRule,
64
- "no-chained-type-assertions": noChainedTypeAssertionsRule,
65
- "no-conditional-empty-object-spread": noConditionalEmptyObjectSpreadRule,
66
- "no-known-value-widening": noKnownValueWideningRule,
67
- "no-module-mocking": noModuleMockingRule,
68
- "no-object-parameters": noObjectParametersRule,
69
- "no-reflect-apply": noReflectApplyRule,
70
- "no-reflect-get": noReflectGetRule,
71
- "no-runtime-typeof": noRuntimeTypeofRule,
72
- "no-shape-in-symbol-names": noForbiddenTermInSymbolNamesRule,
73
- "no-unknown-parameters": noUnknownParametersRule,
74
- "no-unknown-returns": noUnknownReturnsRule,
75
- "no-unknown-type-aliases": noUnknownTypeAliasesRule,
76
- "no-unsafe-dictionary-type": noUnsafeDictionaryTypeRule,
77
- "no-widen-then-assert": noWidenThenAssertRule,
78
- "require-safety-comment-for-type-assertion": requireSafetyCommentForTypeAssertionRule,
79
- "no-call-only-assertions": noCallOnlyAssertionsRule,
80
- "no-pass-through-type-alias": noPassThroughTypeAliasRule,
81
- // Opt-in: NOT in `recommendedRules`. Requires a `cssPath` option, so users add it manually.
82
- "canonical-class-names": canonicalClassNames,
83
- },
84
- }) as ReturnType<typeof eslintCompatPlugin> & { configs: typeof configs };
85
- deslopPlugin.configs = configs;
86
-
87
- export default deslopPlugin;
@@ -1,28 +0,0 @@
1
- import { defineRule } from "@oxlint/plugins";
2
-
3
- import { arrayMethodTarget, isKnownArrayExpression, unwrapArrayExpression } from "../shared/array-method.ts";
4
-
5
- /** Reject eager array filter/map pipelines; lazy iterator helpers remain allowed. */
6
- export const noArrayFilterMapRule = defineRule({
7
- meta: {
8
- type: "suggestion",
9
- docs: { description: "Disallow adjacent array filter/map passes in favor of lazy iterator helpers or a single transformation." },
10
- messages: {
11
- arrayFilterMap: "Avoid consecutive array `{{first}}` and `{{second}}` passes. Prefer `.values().{{first}}(...).{{second}}(...).toArray()` where iterator helpers are supported, or a single `flatMap`/mutating reducer. Preserve callback ordering, indexes, and filtering semantics.",
12
- },
13
- },
14
- createOnce(context) {
15
- return {
16
- CallExpression(node) {
17
- const outer = arrayMethodTarget(node.callee);
18
- if (outer === null || (outer.name !== "map" && outer.name !== "filter")) return;
19
- const innerCall = unwrapArrayExpression(outer.object);
20
- if (innerCall.type !== "CallExpression") return;
21
- const inner = arrayMethodTarget(innerCall.callee);
22
- if (inner === null || inner.name !== (outer.name === "map" ? "filter" : "map")) return;
23
- if (!isKnownArrayExpression(context.sourceCode, inner.object)) return;
24
- context.report({ node, messageId: "arrayFilterMap", data: { first: inner.name, second: outer.name } });
25
- },
26
- };
27
- },
28
- });
@@ -1,94 +0,0 @@
1
- import { defineRule } from "@oxlint/plugins";
2
- import type { ESTree } from "@oxlint/plugins";
3
-
4
- // Tests whose every assertion is a "was it called" mock check verify orchestration, not behavior.
5
- // `toHaveBeenCalledWith` is allowed because it asserts on real data.
6
-
7
- const CALL_ONLY_MATCHERS = new Set(["toHaveBeenCalled", "toHaveBeenCalledTimes", "toHaveBeenCalledOnce"]);
8
- const TEST_NAMES = new Set(["it", "test"]);
9
- const MODIFIERS = new Set(["not", "resolves", "rejects"]);
10
-
11
- function rootIdentifierName(node: ESTree.Expression): string | undefined {
12
- let current: ESTree.Expression = node;
13
- while (true) {
14
- if (current.type === "Identifier") return current.name;
15
- if (current.type === "MemberExpression") {
16
- current = current.object;
17
- } else if (current.type === "CallExpression" || current.type === "TaggedTemplateExpression") {
18
- current = current.type === "CallExpression" ? current.callee : current.tag;
19
- } else {
20
- return undefined;
21
- }
22
- }
23
- }
24
-
25
- function isTestCall(node: ESTree.CallExpression): boolean {
26
- const name = rootIdentifierName(node.callee);
27
- if (name === undefined || !TEST_NAMES.has(name)) return false;
28
- const callback = node.arguments.at(-1);
29
- return callback?.type === "FunctionExpression" || callback?.type === "ArrowFunctionExpression";
30
- }
31
-
32
- function matcherName(node: ESTree.CallExpression): string | undefined {
33
- if (node.callee.type !== "MemberExpression" || node.callee.computed) return undefined;
34
- if (node.callee.property.type !== "Identifier") return undefined;
35
- let object: ESTree.Expression = node.callee.object;
36
- while (
37
- object.type === "MemberExpression" &&
38
- !object.computed &&
39
- object.property.type === "Identifier" &&
40
- MODIFIERS.has(object.property.name)
41
- ) {
42
- object = object.object;
43
- }
44
- const isExpectCall = object.type === "CallExpression" && rootIdentifierName(object.callee) === "expect";
45
- return isExpectCall ? node.callee.property.name : undefined;
46
- }
47
-
48
- interface TestEntry {
49
- node: ESTree.CallExpression;
50
- total: number;
51
- callOnly: number;
52
- }
53
-
54
- /** Tests must assert on behavior, not only that a mock was called. */
55
- export const noCallOnlyAssertionsRule = defineRule({
56
- meta: {
57
- type: "suggestion",
58
- docs: { description: "Tests must assert on behavior, not only that a mock was called" },
59
- messages: {
60
- callOnlyAssertions:
61
- "Every assertion in this test only checks that a mock was called. First check whether this is a valuable test; remove it if not. Otherwise, assert on returned values, state, or call arguments instead.",
62
- },
63
- schema: [],
64
- },
65
- createOnce(context) {
66
- let tests: Array<TestEntry> = [];
67
-
68
- return {
69
- before() {
70
- tests = [];
71
- },
72
- CallExpression(node) {
73
- if (isTestCall(node)) {
74
- tests.push({ node, total: 0, callOnly: 0 });
75
- return;
76
- }
77
- const test = tests.at(-1);
78
- if (!test) return;
79
- const matcher = matcherName(node);
80
- if (!matcher) return;
81
- test.total += 1;
82
- if (CALL_ONLY_MATCHERS.has(matcher)) test.callOnly += 1;
83
- },
84
- "CallExpression:exit"(node) {
85
- const test = tests.at(-1);
86
- if (test?.node !== node) return;
87
- tests.pop();
88
- if (test.total > 0 && test.total === test.callOnly) {
89
- context.report({ messageId: "callOnlyAssertions", node: node.callee });
90
- }
91
- },
92
- };
93
- },
94
- });