adamantite 0.41.0 → 0.43.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.
Files changed (39) hide show
  1. package/README.md +393 -35
  2. package/dist/index.js +47 -28
  3. package/dist/presets/analyze.d.ts +6 -2
  4. package/dist/presets/analyze.js +7 -1
  5. package/dist/presets/lint/core.d.ts +5 -3
  6. package/dist/presets/lint/core.js +1 -0
  7. package/dist/presets/lint/custom.d.ts +60 -0
  8. package/dist/presets/lint/custom.js +142 -0
  9. package/dist/presets/lint/effect.d.ts +5 -0
  10. package/dist/presets/lint/effect.js +106 -0
  11. package/dist/presets/lint/plugin/imports.d.ts +19 -0
  12. package/dist/presets/lint/plugin/imports.js +53 -0
  13. package/dist/presets/lint/plugin/index.d.ts +17 -0
  14. package/dist/presets/lint/plugin/index.js +20 -0
  15. package/dist/presets/lint/plugin/options.d.ts +11 -0
  16. package/dist/presets/lint/plugin/options.js +33 -0
  17. package/dist/presets/lint/plugin/rules/no-overzealous-destructuring.d.ts +5 -0
  18. package/dist/presets/lint/plugin/rules/no-overzealous-destructuring.js +92 -0
  19. package/dist/presets/lint/plugin/rules/no-query-data-in-state.d.ts +5 -0
  20. package/dist/presets/lint/plugin/rules/no-query-data-in-state.js +85 -0
  21. package/dist/presets/lint/plugin/rules/no-react-state-hooks.d.ts +10 -0
  22. package/dist/presets/lint/plugin/rules/no-react-state-hooks.js +128 -0
  23. package/dist/presets/lint/plugin/rules/query-from-loader.d.ts +5 -0
  24. package/dist/presets/lint/plugin/rules/query-from-loader.js +110 -0
  25. package/dist/presets/lint/react-doctor.d.ts +5 -0
  26. package/dist/presets/lint/react-doctor.js +20 -0
  27. package/dist/presets/lint/react-strict.d.ts +5 -0
  28. package/dist/presets/lint/react-strict.js +11 -0
  29. package/dist/presets/lint/shadcn.d.ts +5 -0
  30. package/dist/presets/lint/shadcn.js +22 -0
  31. package/dist/presets/lint/strict.d.ts +5 -0
  32. package/dist/presets/lint/strict.js +18 -0
  33. package/dist/presets/lint/tanstack.d.ts +5 -0
  34. package/dist/presets/lint/tanstack.js +14 -0
  35. package/dist/presets/lint/vendor/antislop/plugin.d.mts +18 -2
  36. package/dist/presets/rules/index.d.ts +9 -0
  37. package/dist/presets/rules/index.js +29 -0
  38. package/package.json +31 -22
  39. package/skills/adamantite/SKILL.md +105 -11
@@ -0,0 +1,60 @@
1
+ import { DummyRule, OxlintConfig } from "oxlint";
2
+ //#region presets/lint/custom.d.ts
3
+ /**
4
+ * The plugin name that `custom()` uses when no `name` is given.
5
+ */
6
+ export declare const DEFAULT_PLUGIN_NAME = "project";
7
+ /**
8
+ * The rule name that a rule file defines: its file name without the extension.
9
+ */
10
+ export declare function getRuleName(file: string): string;
11
+ /**
12
+ * Whether a file in a rules folder is a rule: it has a JavaScript or TypeScript extension, is not a
13
+ * declaration file, and does not start with `_`.
14
+ */
15
+ export declare function isRuleFile(name: string): boolean;
16
+ /**
17
+ * The rule files in a rules folder. Subfolders are not read, so helpers can also live there.
18
+ */
19
+ export declare function listRuleFiles(dir: string): string[];
20
+ export interface CustomRulesOptions {
21
+ /**
22
+ * The rules folder. A relative path resolves from the file that calls `custom()`, not from the
23
+ * working directory. Default: `.adamantite/rules`.
24
+ */
25
+ readonly dir?: string;
26
+ /**
27
+ * The plugin name, which prefixes every rule, as in `project/no-process-env`. Oxlint rejects two
28
+ * plugins with the same name in one run, so give each rules folder in a monorepo its own name.
29
+ * Default: `project`.
30
+ */
31
+ readonly name?: string;
32
+ /**
33
+ * Severity or options for a rule, by rule name without the plugin prefix. Every rule is enabled
34
+ * as `"error"` by default.
35
+ */
36
+ readonly rules?: Readonly<Record<string, DummyRule>>;
37
+ }
38
+ /**
39
+ * The first file in a stack trace that is not `ownFile`: the module that called into `ownFile`.
40
+ * Undefined when the stack names no such file.
41
+ */
42
+ export declare function findCallerFile(stack: string, ownFile: string): string | undefined;
43
+ /**
44
+ * Load the custom rules in a rules folder and enable each one as `error`. A rule file
45
+ * `no-process-env.ts` becomes the rule `project/no-process-env`. Files that start with `_` are
46
+ * helpers, not rules.
47
+ *
48
+ * Without a rules folder, or with an empty one, the result is an empty config, so every project can
49
+ * extend it.
50
+ *
51
+ * ```ts
52
+ * import { defineConfig } from "oxlint"
53
+ * import core from "adamantite/lint"
54
+ * import custom from "adamantite/lint/custom"
55
+ *
56
+ * export default defineConfig({ extends: [core, custom()] })
57
+ * ```
58
+ */
59
+ export default function custom(options?: CustomRulesOptions): OxlintConfig;
60
+ //#endregion
@@ -0,0 +1,142 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, extname, isAbsolute, join, resolve } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ //#region presets/lint/custom.ts
7
+ const MODULE_EXTENSION = import.meta.url.endsWith(".ts") ? "ts" : "js";
8
+ const RULES_MODULE_URL = new URL(`../rules/index.${MODULE_EXTENSION}`, import.meta.url).href;
9
+ const DEFAULT_DIRECTORY = ".adamantite/rules";
10
+ const CACHE_DIRECTORY = join("node_modules", ".cache", "adamantite");
11
+ /**
12
+ * The plugin name that `custom()` uses when no `name` is given.
13
+ */
14
+ const DEFAULT_PLUGIN_NAME = "project";
15
+ const RULE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
16
+ ".js",
17
+ ".mjs",
18
+ ".mts",
19
+ ".ts"
20
+ ]);
21
+ /**
22
+ * The rule name that a rule file defines: its file name without the extension.
23
+ */
24
+ function getRuleName(file) {
25
+ return file.slice(0, -extname(file).length);
26
+ }
27
+ /**
28
+ * Whether a file in a rules folder is a rule: it has a JavaScript or TypeScript extension, is not a
29
+ * declaration file, and does not start with `_`.
30
+ */
31
+ function isRuleFile(name) {
32
+ return RULE_FILE_EXTENSIONS.has(extname(name)) && !name.startsWith("_") && !/\.d\.m?ts$/.test(name);
33
+ }
34
+ /**
35
+ * The rule files in a rules folder. Subfolders are not read, so helpers can also live there.
36
+ */
37
+ function listRuleFiles(dir) {
38
+ return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isFile() && isRuleFile(entry.name)).map((entry) => entry.name);
39
+ }
40
+ /**
41
+ * Normalize a file from a stack frame: drop the query string and hash, which Oxlint adds to bypass
42
+ * the module cache, and convert a file URL to a path.
43
+ */
44
+ function toFramePath(location) {
45
+ if (location.startsWith("file://")) {
46
+ const url = new URL(location);
47
+ url.search = "";
48
+ url.hash = "";
49
+ return fileURLToPath(url);
50
+ }
51
+ return location.replace(/[?#].*$/, "");
52
+ }
53
+ const OWN_FILE = toFramePath(import.meta.url);
54
+ const FRAME_LOCATION = /(?:\(|at )((?:file:\/\/|\/|[A-Za-z]:[\\/]).+?):\d+:\d+\)?\s*$/;
55
+ /**
56
+ * The first file in a stack trace that is not `ownFile`: the module that called into `ownFile`.
57
+ * Undefined when the stack names no such file.
58
+ */
59
+ function findCallerFile(stack, ownFile) {
60
+ for (const line of stack.split("\n").slice(1)) {
61
+ const location = FRAME_LOCATION.exec(line)?.[1];
62
+ if (location === void 0) continue;
63
+ const file = toFramePath(location);
64
+ if (file !== ownFile) return file;
65
+ }
66
+ }
67
+ function resolveRulesDirectory(dir, stack) {
68
+ if (isAbsolute(dir)) return dir;
69
+ const caller = findCallerFile(stack, OWN_FILE);
70
+ if (caller === void 0) throw new Error(`custom() could not find the file that called it, so it cannot resolve "${dir}". Pass an absolute \`dir\`, such as \`join(import.meta.dirname, "${dir}")\`.`);
71
+ return resolve(dirname(caller), dir);
72
+ }
73
+ function findNodeModules(from) {
74
+ for (let current = from;; current = dirname(current)) {
75
+ if (existsSync(join(current, "node_modules"))) return current;
76
+ if (dirname(current) === current) return;
77
+ }
78
+ }
79
+ function writeIfChanged(path, content) {
80
+ if (existsSync(path) && readFileSync(path, "utf8") === content) return;
81
+ mkdirSync(dirname(path), { recursive: true });
82
+ writeFileSync(path, content);
83
+ }
84
+ /**
85
+ * Write the entry module for a rules folder and return its file URL. The module goes to
86
+ * `node_modules/.cache/adamantite` above the folder, or to the OS temporary directory when no
87
+ * `node_modules` folder is found or it cannot be written.
88
+ */
89
+ function writeEntryModule(dir, name) {
90
+ const fileName = `rules-${createHash("sha256").update(`${dir}\0${name}`).digest("hex").slice(0, 16)}.mjs`;
91
+ const content = [
92
+ `import { loadRules } from ${JSON.stringify(RULES_MODULE_URL)}`,
93
+ "",
94
+ `export default await loadRules(${JSON.stringify(dir)}, ${JSON.stringify(name)})`,
95
+ ""
96
+ ].join("\n");
97
+ const root = findNodeModules(dir);
98
+ const candidates = [...root === void 0 ? [] : [join(root, CACHE_DIRECTORY, fileName)], join(tmpdir(), "adamantite", fileName)];
99
+ const failures = [];
100
+ for (const path of candidates) try {
101
+ writeIfChanged(path, content);
102
+ return pathToFileURL(path).href;
103
+ } catch (error) {
104
+ failures.push(error);
105
+ }
106
+ throw new AggregateError(failures, `custom() could not write the entry module for ${dir}.`);
107
+ }
108
+ /**
109
+ * Load the custom rules in a rules folder and enable each one as `error`. A rule file
110
+ * `no-process-env.ts` becomes the rule `project/no-process-env`. Files that start with `_` are
111
+ * helpers, not rules.
112
+ *
113
+ * Without a rules folder, or with an empty one, the result is an empty config, so every project can
114
+ * extend it.
115
+ *
116
+ * ```ts
117
+ * import { defineConfig } from "oxlint"
118
+ * import core from "adamantite/lint"
119
+ * import custom from "adamantite/lint/custom"
120
+ *
121
+ * export default defineConfig({ extends: [core, custom()] })
122
+ * ```
123
+ */
124
+ function custom(options = {}) {
125
+ const name = options.name ?? "project";
126
+ const dir = resolveRulesDirectory(options.dir ?? DEFAULT_DIRECTORY, (/* @__PURE__ */ new Error("custom()")).stack ?? "");
127
+ if (!existsSync(dir)) return {};
128
+ const files = listRuleFiles(dir);
129
+ if (files.length === 0) return {};
130
+ const rules = {};
131
+ for (const file of files) rules[`${name}/${getRuleName(file)}`] = "error";
132
+ for (const [rule, setting] of Object.entries(options.rules ?? {})) rules[`${name}/${rule}`] = setting;
133
+ return {
134
+ jsPlugins: [{
135
+ name,
136
+ specifier: writeEntryModule(dir, name)
137
+ }],
138
+ rules
139
+ };
140
+ }
141
+ //#endregion
142
+ export { DEFAULT_PLUGIN_NAME, custom as default, findCallerFile, getRuleName, isRuleFile, listRuleFiles };
@@ -0,0 +1,5 @@
1
+ import { OxlintConfig } from "oxlint";
2
+ //#region presets/lint/effect.d.ts
3
+ declare const config: OxlintConfig;
4
+ //#endregion
5
+ export { config as default };
@@ -0,0 +1,106 @@
1
+ //#region presets/lint/effect.ts
2
+ const config = {
3
+ options: { typeAware: true },
4
+ plugins: ["effecttsgo"],
5
+ rules: {
6
+ "effecttsgo/any-unknown-in-error-context": "error",
7
+ "effecttsgo/class-self-mismatch": "error",
8
+ "effecttsgo/duplicate-package": "error",
9
+ "effecttsgo/effect-fn-implicit-any": "error",
10
+ "effecttsgo/experimental-api-usage": "error",
11
+ "effecttsgo/floating-effect": "error",
12
+ "effecttsgo/floating-effect-in-vitest": "error",
13
+ "effecttsgo/generic-effect-services": "error",
14
+ "effecttsgo/missing-effect-context": "error",
15
+ "effecttsgo/missing-effect-error": "error",
16
+ "effecttsgo/missing-layer-context": "error",
17
+ "effecttsgo/missing-return-yield-star": "error",
18
+ "effecttsgo/missing-star-in-yield-effect-gen": "error",
19
+ "effecttsgo/non-object-effect-service-type": "error",
20
+ "effecttsgo/obsolete-match-import": "error",
21
+ "effecttsgo/obsolete-schema-import": "error",
22
+ "effecttsgo/outdated-api": "error",
23
+ "effecttsgo/overridden-schema-constructor": "error",
24
+ "effecttsgo/promise-in-effect-success": "error",
25
+ "effecttsgo/schema-literal-non-finite": "error",
26
+ "effecttsgo/schema-opaque-instance-member": "error",
27
+ "effecttsgo/unsafe-effect-type-assertion": "error",
28
+ "effecttsgo/unstable-api-usage": "error",
29
+ "effecttsgo/catch-unfailable-effect": "error",
30
+ "effecttsgo/effect-fn-iife": "error",
31
+ "effecttsgo/effect-gen-uses-adapter": "error",
32
+ "effecttsgo/effect-in-failure": "error",
33
+ "effecttsgo/effect-in-void-success": "error",
34
+ "effecttsgo/global-error-in-effect-catch": "error",
35
+ "effecttsgo/global-error-in-effect-failure": "error",
36
+ "effecttsgo/layer-merge-all-with-dependencies": "error",
37
+ "effecttsgo/lazy-effect": "error",
38
+ "effecttsgo/lazy-promise-in-effect-sync": "error",
39
+ "effecttsgo/leaking-requirements": "error",
40
+ "effecttsgo/multiple-effect-provide": "error",
41
+ "effecttsgo/prefer-unsafe-constructor": "error",
42
+ "effecttsgo/return-effect-in-gen": "error",
43
+ "effecttsgo/run-effect-inside-effect": "error",
44
+ "effecttsgo/schema-sync-in-effect": "error",
45
+ "effecttsgo/scope-in-layer-effect": "error",
46
+ "effecttsgo/try-catch-in-effect-gen": "error",
47
+ "effecttsgo/unknown-in-effect-catch": "error",
48
+ "effecttsgo/abort-controller-in-effect": "error",
49
+ "effecttsgo/crypto-random-uuid-in-effect": "error",
50
+ "effecttsgo/global-console-in-effect": "error",
51
+ "effecttsgo/global-date-in-effect": "error",
52
+ "effecttsgo/global-fetch-in-effect": "error",
53
+ "effecttsgo/global-random-in-effect": "error",
54
+ "effecttsgo/global-timers-in-effect": "error",
55
+ "effecttsgo/instance-of-schema": "error",
56
+ "effecttsgo/process-env-in-effect": "error",
57
+ "effecttsgo/acquire-release-disposable": "error",
58
+ "effecttsgo/all-of-map-to-for-each": "error",
59
+ "effecttsgo/catch-all-tag-dispatch-to-catch-tag": "error",
60
+ "effecttsgo/catch-all-to-map-error": "error",
61
+ "effecttsgo/catch-chain-to-first-success-of": "error",
62
+ "effecttsgo/catch-conditional-refail-to-catch-if": "error",
63
+ "effecttsgo/catch-die-to-or-die": "error",
64
+ "effecttsgo/catch-if-tag-to-catch-tag": "error",
65
+ "effecttsgo/catch-refail-to-tap-error": "error",
66
+ "effecttsgo/catch-tag-to-catch-reason": "error",
67
+ "effecttsgo/catch-to-ignore": "error",
68
+ "effecttsgo/catch-to-or-else-succeed": "error",
69
+ "effecttsgo/effect-do-notation": "error",
70
+ "effecttsgo/effect-fn-opportunity": "error",
71
+ "effecttsgo/effect-map-flatten": "error",
72
+ "effecttsgo/effect-map-void": "error",
73
+ "effecttsgo/effect-succeed-with-void": "error",
74
+ "effecttsgo/flat-map-conditional-to-filter-or-fail": "error",
75
+ "effecttsgo/flat-map-ignored-param-to-and-then": "error",
76
+ "effecttsgo/flat-map-to-map": "error",
77
+ "effecttsgo/map-some-to-as-some": "error",
78
+ "effecttsgo/match-effect-to-map-both": "error",
79
+ "effecttsgo/match-effect-to-match": "error",
80
+ "effecttsgo/multiple-catch-tag": "error",
81
+ "effecttsgo/nested-effect-gen-yield": "error",
82
+ "effecttsgo/option-match-to-from-option": "error",
83
+ "effecttsgo/prefer-schema-type-property": "error",
84
+ "effecttsgo/prefer-succeed-some-or-none": "error",
85
+ "effecttsgo/prefer-typed-schema-decoder": "error",
86
+ "effecttsgo/provide-layer-succeed-to-provide-service": "error",
87
+ "effecttsgo/race-first-with-sleep-to-timeout": "error",
88
+ "effecttsgo/redundant-map-error": "error",
89
+ "effecttsgo/redundant-or-die": "error",
90
+ "effecttsgo/redundant-schema-tag-identifier": "error",
91
+ "effecttsgo/run-of-exit-to-run-exit": "error",
92
+ "effecttsgo/schema-number": "error",
93
+ "effecttsgo/schema-struct-with-tag": "error",
94
+ "effecttsgo/schema-union-of-literals": "error",
95
+ "effecttsgo/service-not-as-class": "error",
96
+ "effecttsgo/sync-to-succeed": "error",
97
+ "effecttsgo/timeout-catch-tag-to-timeout-or-else": "error",
98
+ "effecttsgo/unnecessary-effect-gen": "error",
99
+ "effecttsgo/unnecessary-fail-yieldable-error": "error",
100
+ "effecttsgo/unnecessary-pipe": "error",
101
+ "effecttsgo/unnecessary-pipe-chain": "error",
102
+ "effecttsgo/unnecessary-typeof-type": "error"
103
+ }
104
+ };
105
+ //#endregion
106
+ export { config as default };
@@ -0,0 +1,19 @@
1
+ import { Context, ESTree } from "@oxlint/plugins";
2
+ //#region presets/lint/plugin/imports.d.ts
3
+ /**
4
+ * A call of a module export, such as `useState(0)` or `React.useState(0)`.
5
+ */
6
+ export interface ImportedCall {
7
+ readonly call: ESTree.CallExpression;
8
+ /**
9
+ * The export name, which stays the same when the import is aliased.
10
+ */
11
+ readonly name: string;
12
+ }
13
+ /**
14
+ * Find the calls of the exports in `names` that `node` imports: direct calls through a named
15
+ * import, including aliased ones, and member calls such as `React.useState(0)` through a default or
16
+ * namespace import. Type-only imports give no calls.
17
+ */
18
+ export declare function findImportedCalls(context: Context, node: ESTree.ImportDeclaration, names: ReadonlySet<string>): ImportedCall[];
19
+ //#endregion
@@ -0,0 +1,53 @@
1
+ //#region presets/lint/plugin/imports.ts
2
+ function getImportedName(specifier) {
3
+ return specifier.imported.type === "Literal" ? specifier.imported.value : specifier.imported.name;
4
+ }
5
+ function isStringLiteral(node) {
6
+ return node.type === "Literal" && typeof node.value === "string";
7
+ }
8
+ function getPropertyName(member) {
9
+ if (!member.computed) return member.property.type === "Identifier" ? member.property.name : void 0;
10
+ return isStringLiteral(member.property) ? member.property.value : void 0;
11
+ }
12
+ function getCall(node) {
13
+ const { parent } = node;
14
+ return parent?.type === "CallExpression" && parent.callee === node ? parent : void 0;
15
+ }
16
+ /**
17
+ * Find the calls of the exports in `names` that `node` imports: direct calls through a named
18
+ * import, including aliased ones, and member calls such as `React.useState(0)` through a default or
19
+ * namespace import. Type-only imports give no calls.
20
+ */
21
+ function findImportedCalls(context, node, names) {
22
+ if (node.importKind === "type") return [];
23
+ const calls = [];
24
+ for (const specifier of node.specifiers) {
25
+ if (specifier.type === "ImportSpecifier" && specifier.importKind === "type") continue;
26
+ const identifiers = context.sourceCode.getDeclaredVariables(specifier).flatMap((variable) => variable.references).map((reference) => reference.identifier);
27
+ if (specifier.type === "ImportSpecifier") {
28
+ const name = getImportedName(specifier);
29
+ if (!names.has(name)) continue;
30
+ for (const identifier of identifiers) {
31
+ const call = getCall(identifier);
32
+ if (call !== void 0) calls.push({
33
+ call,
34
+ name
35
+ });
36
+ }
37
+ continue;
38
+ }
39
+ for (const identifier of identifiers) {
40
+ const member = identifier.parent;
41
+ if (member.type !== "MemberExpression" || member.object !== identifier) continue;
42
+ const name = getPropertyName(member);
43
+ const call = getCall(member);
44
+ if (name !== void 0 && names.has(name) && call !== void 0) calls.push({
45
+ call,
46
+ name
47
+ });
48
+ }
49
+ }
50
+ return calls;
51
+ }
52
+ //#endregion
53
+ export { findImportedCalls };
@@ -0,0 +1,17 @@
1
+ import { Plugin, Rule } from "@oxlint/plugins";
2
+ //#region presets/lint/plugin/index.d.ts
3
+ interface AdamantitePlugin extends Plugin {
4
+ readonly rules: {
5
+ readonly "no-overzealous-destructuring": Rule;
6
+ readonly "no-query-data-in-state": Rule;
7
+ readonly "no-react-state-hooks": Rule;
8
+ readonly "query-from-loader": Rule;
9
+ };
10
+ }
11
+ /**
12
+ * Adamantite's first-party Oxlint plugin. Presets load it by file URL, so target projects install
13
+ * nothing extra.
14
+ */
15
+ declare const plugin: AdamantitePlugin;
16
+ //#endregion
17
+ export { plugin as default };
@@ -0,0 +1,20 @@
1
+ import rule from "./rules/no-overzealous-destructuring.js";
2
+ import rule$1 from "./rules/no-query-data-in-state.js";
3
+ import rule$2 from "./rules/no-react-state-hooks.js";
4
+ import rule$3 from "./rules/query-from-loader.js";
5
+ //#region presets/lint/plugin/index.ts
6
+ /**
7
+ * Adamantite's first-party Oxlint plugin. Presets load it by file URL, so target projects install
8
+ * nothing extra.
9
+ */
10
+ const plugin = {
11
+ meta: { name: "adamantite" },
12
+ rules: {
13
+ "no-overzealous-destructuring": rule,
14
+ "no-query-data-in-state": rule$1,
15
+ "no-react-state-hooks": rule$2,
16
+ "query-from-loader": rule$3
17
+ }
18
+ };
19
+ //#endregion
20
+ export { plugin as default };
@@ -0,0 +1,11 @@
1
+ import { Options } from "@oxlint/plugins";
2
+ //#region presets/lint/plugin/options.d.ts
3
+ /**
4
+ * Read a string array from the rule's first option object.
5
+ */
6
+ export declare function readStringArrayOption(options: Readonly<Options>, key: string): string[] | undefined;
7
+ /**
8
+ * Read a positive integer from the rule's first option object.
9
+ */
10
+ export declare function readPositiveIntegerOption(options: Readonly<Options>, key: string): number | undefined;
11
+ //#endregion
@@ -0,0 +1,33 @@
1
+ //#region presets/lint/plugin/options.ts
2
+ function isOptionObject(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4
+ }
5
+ function isString(value) {
6
+ return typeof value === "string";
7
+ }
8
+ function isStringArray(value) {
9
+ return Array.isArray(value) && value.every((entry) => isString(entry));
10
+ }
11
+ function isPositiveInteger(value) {
12
+ return typeof value === "number" && Number.isInteger(value) && value > 0;
13
+ }
14
+ function readOption(options, key) {
15
+ const [first] = options;
16
+ return isOptionObject(first) ? first[key] : void 0;
17
+ }
18
+ /**
19
+ * Read a string array from the rule's first option object.
20
+ */
21
+ function readStringArrayOption(options, key) {
22
+ const value = readOption(options, key);
23
+ return isStringArray(value) ? value : void 0;
24
+ }
25
+ /**
26
+ * Read a positive integer from the rule's first option object.
27
+ */
28
+ function readPositiveIntegerOption(options, key) {
29
+ const value = readOption(options, key);
30
+ return isPositiveInteger(value) ? value : void 0;
31
+ }
32
+ //#endregion
33
+ export { readPositiveIntegerOption, readStringArrayOption };
@@ -0,0 +1,5 @@
1
+ import { Rule } from "@oxlint/plugins";
2
+ //#region presets/lint/plugin/rules/no-overzealous-destructuring.d.ts
3
+ declare const rule: Rule;
4
+ //#endregion
5
+ export { rule as default };
@@ -0,0 +1,92 @@
1
+ import { readPositiveIntegerOption } from "../options.js";
2
+ //#region presets/lint/plugin/rules/no-overzealous-destructuring.ts
3
+ /**
4
+ * `const { data: { user } } = query` nests two patterns, which stays allowed.
5
+ */
6
+ const DEFAULT_MAX_DEPTH = 2;
7
+ /**
8
+ * Five properties cover typical component props. A rest element does not count.
9
+ */
10
+ const DEFAULT_MAX_PROPERTIES = 5;
11
+ function isPattern(node) {
12
+ return node.type === "ObjectPattern" || node.type === "ArrayPattern";
13
+ }
14
+ /**
15
+ * The pattern that directly contains `pattern`, through a property, a default value, or a rest
16
+ * element. A root pattern, such as a declaration target or a parameter, has none.
17
+ */
18
+ function getParentPattern(pattern) {
19
+ let node = pattern.parent;
20
+ while (node.type === "AssignmentPattern" || node.type === "RestElement") node = node.parent;
21
+ if (node.type === "Property" && node.parent.type === "ObjectPattern") return node.parent;
22
+ return isPattern(node) ? node : void 0;
23
+ }
24
+ function getDepth(pattern) {
25
+ let depth = 1;
26
+ let parent = getParentPattern(pattern);
27
+ while (parent !== void 0) {
28
+ depth += 1;
29
+ parent = getParentPattern(parent);
30
+ }
31
+ return depth;
32
+ }
33
+ const rule = {
34
+ create(context) {
35
+ const maxDepth = readPositiveIntegerOption(context.options, "maxDepth") ?? DEFAULT_MAX_DEPTH;
36
+ const maxProperties = readPositiveIntegerOption(context.options, "maxProperties") ?? DEFAULT_MAX_PROPERTIES;
37
+ function checkDepth(node) {
38
+ const depth = getDepth(node);
39
+ if (depth === maxDepth + 1) context.report({
40
+ data: {
41
+ depth: String(depth),
42
+ maxDepth: String(maxDepth)
43
+ },
44
+ messageId: "tooDeep",
45
+ node
46
+ });
47
+ }
48
+ return {
49
+ ArrayPattern: checkDepth,
50
+ ObjectPattern(node) {
51
+ checkDepth(node);
52
+ const count = node.properties.filter((property) => property.type === "Property").length;
53
+ if (count > maxProperties) context.report({
54
+ data: {
55
+ count: String(count),
56
+ maxProperties: String(maxProperties)
57
+ },
58
+ messageId: "tooManyProperties",
59
+ node
60
+ });
61
+ }
62
+ };
63
+ },
64
+ meta: {
65
+ defaultOptions: [{
66
+ maxDepth: DEFAULT_MAX_DEPTH,
67
+ maxProperties: DEFAULT_MAX_PROPERTIES
68
+ }],
69
+ docs: { description: "Disallow destructuring patterns that nest too deep or take too many properties from one object." },
70
+ messages: {
71
+ tooDeep: "This destructuring pattern is nested {{depth}} levels deep. The limit is {{maxDepth}}. Keep the outer object and read its members, or split the pattern into separate declarations.",
72
+ tooManyProperties: "This destructuring pattern takes {{count}} properties from one object. The limit is {{maxProperties}}. Keep the object and read its members where you use them, or split the pattern."
73
+ },
74
+ schema: [{
75
+ additionalProperties: false,
76
+ properties: {
77
+ maxDepth: {
78
+ minimum: 1,
79
+ type: "integer"
80
+ },
81
+ maxProperties: {
82
+ minimum: 1,
83
+ type: "integer"
84
+ }
85
+ },
86
+ type: "object"
87
+ }],
88
+ type: "suggestion"
89
+ }
90
+ };
91
+ //#endregion
92
+ export { rule as default };
@@ -0,0 +1,5 @@
1
+ import { Rule } from "@oxlint/plugins";
2
+ //#region presets/lint/plugin/rules/no-query-data-in-state.d.ts
3
+ declare const rule: Rule;
4
+ //#endregion
5
+ export { rule as default };
@@ -0,0 +1,85 @@
1
+ import { findImportedCalls } from "../imports.js";
2
+ //#region presets/lint/plugin/rules/no-query-data-in-state.ts
3
+ const QUERY_MODULE = "@tanstack/react-query";
4
+ const REACT_MODULE = "react";
5
+ const QUERY_HOOKS = /* @__PURE__ */ new Set([
6
+ "useInfiniteQuery",
7
+ "useQuery",
8
+ "useSuspenseInfiniteQuery",
9
+ "useSuspenseQuery"
10
+ ]);
11
+ const STATE_HOOKS = /* @__PURE__ */ new Set(["useState"]);
12
+ function isDataKey(property) {
13
+ return !property.computed && property.key.type === "Identifier" && property.key.name === "data";
14
+ }
15
+ function getReferences(context, declarator, name) {
16
+ return context.sourceCode.getDeclaredVariables(declarator).filter((variable) => variable.name === name).flatMap((variable) => variable.references).map((reference) => reference.identifier);
17
+ }
18
+ /**
19
+ * The expressions that read the data of the query result that `call` returns: `query.data` for
20
+ * `const query = useQuery(...)`, and each `data` read for `const { data } = useQuery(...)`.
21
+ */
22
+ function findDataReads(context, call) {
23
+ const declarator = call.parent;
24
+ if (declarator.type !== "VariableDeclarator" || declarator.init !== call) return [];
25
+ const { id } = declarator;
26
+ if (id.type === "Identifier") return getReferences(context, declarator, id.name).flatMap((identifier) => {
27
+ const member = identifier.parent;
28
+ return member.type === "MemberExpression" && member.object === identifier && !member.computed && member.property.name === "data" ? [member] : [];
29
+ });
30
+ if (id.type !== "ObjectPattern") return [];
31
+ return id.properties.flatMap((property) => {
32
+ if (property.type !== "Property" || !isDataKey(property)) return [];
33
+ const binding = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
34
+ return binding.type === "Identifier" ? getReferences(context, declarator, binding.name) : [];
35
+ });
36
+ }
37
+ /**
38
+ * Whether `node` evaluates to query data or to a value inside it, such as `query.data`,
39
+ * `data.user.name`, `data ?? []`, or `() => data`.
40
+ */
41
+ function readsData(node, dataReads) {
42
+ if (dataReads.has(node)) return true;
43
+ switch (node.type) {
44
+ case "ArrowFunctionExpression": return node.expression && node.body.type !== "BlockStatement" && readsData(node.body, dataReads);
45
+ case "ChainExpression":
46
+ case "TSAsExpression":
47
+ case "TSNonNullExpression":
48
+ case "TSSatisfiesExpression": return readsData(node.expression, dataReads);
49
+ case "LogicalExpression": return readsData(node.left, dataReads) || readsData(node.right, dataReads);
50
+ case "MemberExpression": return readsData(node.object, dataReads);
51
+ default: return false;
52
+ }
53
+ }
54
+ const rule = {
55
+ create(context) {
56
+ const queryCalls = [];
57
+ const stateCalls = [];
58
+ return {
59
+ ImportDeclaration(node) {
60
+ if (node.source.value === QUERY_MODULE) queryCalls.push(...findImportedCalls(context, node, QUERY_HOOKS));
61
+ else if (node.source.value === REACT_MODULE) stateCalls.push(...findImportedCalls(context, node, STATE_HOOKS));
62
+ },
63
+ "Program:exit"() {
64
+ if (queryCalls.length === 0 || stateCalls.length === 0) return;
65
+ const dataReads = new Set(queryCalls.flatMap(({ call }) => findDataReads(context, call)));
66
+ for (const { call, name } of stateCalls) {
67
+ const [initial] = call.arguments;
68
+ if (initial !== void 0 && readsData(initial, dataReads)) context.report({
69
+ data: { hook: name },
70
+ messageId: "copiedData",
71
+ node: call
72
+ });
73
+ }
74
+ }
75
+ };
76
+ },
77
+ meta: {
78
+ docs: { description: "Disallow copying TanStack Query data into React state, where the copy stops updating when the query refetches." },
79
+ messages: { copiedData: "`{{hook}}` copies query data into local state, so the copy stops updating when the query refetches or another component updates the cache. Read the value from the query result where you use it. To edit it, keep only the user's changes in state and merge them with the query data during render." },
80
+ schema: [],
81
+ type: "problem"
82
+ }
83
+ };
84
+ //#endregion
85
+ export { rule as default };