hook-o-gnese 0.0.6 → 0.0.7
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 +1 -1
- package/dist/cli.mjs +6 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/{engine-DJFFKwTZ.mjs → engine-D5X2INMx.mjs} +2 -2
- package/dist/{engine-DJFFKwTZ.mjs.map → engine-D5X2INMx.mjs.map} +1 -1
- package/dist/engine.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{registry-iRG6wil9.mjs → registry-YXM4yhf2.mjs} +33 -3
- package/dist/registry-YXM4yhf2.mjs.map +1 -0
- package/package.json +4 -1
- package/dist/registry-iRG6wil9.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ No linter setup required. Works in any project. Outputs stylish, JSON, SARIF
|
|
|
62
62
|
```bash
|
|
63
63
|
npx hook-o-gnese ./src
|
|
64
64
|
npx hook-o-gnese ./src --format=sarif > report.sarif
|
|
65
|
-
npx hook-o-gnese ./src --type-aware # enables custom-hook-depth
|
|
65
|
+
npx hook-o-gnese ./src --type-aware # enables custom-hook-depth (needs `typescript` in your project)
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
Add a `.hookogneserc.json` if you want to tune thresholds:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as isTypescriptAvailable } from "./registry-YXM4yhf2.mjs";
|
|
3
|
+
import { n as lintFiles } from "./engine-D5X2INMx.mjs";
|
|
3
4
|
import { parseArgs } from "node:util";
|
|
4
5
|
import { readFile } from "node:fs/promises";
|
|
5
6
|
import { globby } from "globby";
|
|
@@ -134,7 +135,8 @@ Usage:
|
|
|
134
135
|
Options:
|
|
135
136
|
--format=<fmt> stylish (default) | json | sarif | github
|
|
136
137
|
--config=<path> path to .hookogneserc.json
|
|
137
|
-
--type-aware enable custom-hook-depth (slower, uses TS Compiler API)
|
|
138
|
+
--type-aware enable custom-hook-depth (slower, uses TS Compiler API).
|
|
139
|
+
Requires 'typescript' installed in your project.
|
|
138
140
|
--rule=<id>=<sev> override rule severity (off|warn|error). Repeatable.
|
|
139
141
|
--help, -h show this message
|
|
140
142
|
|
|
@@ -154,7 +156,8 @@ async function runCli(opts, io) {
|
|
|
154
156
|
return 2;
|
|
155
157
|
}
|
|
156
158
|
const { engine, ignore } = await loadConfig(opts.cwd, opts.config, io.readTextFile);
|
|
157
|
-
if (opts.typeAware) engine.typeAware = true;
|
|
159
|
+
if (opts.typeAware) if (isTypescriptAvailable(opts.cwd)) engine.typeAware = true;
|
|
160
|
+
else io.writeStderr("warning: --type-aware was requested but the 'typescript' package is not installed. Skipping type-aware rules. Install with: npm i -D typescript@>=6\n");
|
|
158
161
|
const finalEngine = applyCliRuleOverrides(engine, opts.ruleOverrides);
|
|
159
162
|
const files = await globby(opts.paths, {
|
|
160
163
|
ignore: [...DEFAULT_IGNORE, ...ignore],
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["jsonFmt"],"sources":["../src/config.ts","../src/formatters/stylish.ts","../src/formatters/json.ts","../src/formatters/sarif.ts","../src/formatters/github.ts","../src/cli-core.ts","../src/cli.node.ts"],"sourcesContent":["import type { EngineConfig, Severity } from \"./engine.ts\";\n\nconst DEFAULT_RULES: Record<string, { severity: Severity; options?: unknown }> =\n {\n \"hook-o-gnese/no-fat-effects\": { severity: \"warn\" },\n \"hook-o-gnese/state-scatter\": { severity: \"warn\" },\n \"hook-o-gnese/hook-coupling\": { severity: \"error\" },\n \"hook-o-gnese/custom-hook-depth\": {\n severity: \"warn\",\n options: { maxDepth: 3 },\n },\n };\n\nexport const DEFAULT_IGNORE = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.next/**\",\n \"**/.cache/**\",\n];\n\ninterface FileConfig {\n rules?: Record<string, Severity | [Severity, unknown]>;\n ignore?: string[];\n typeAware?: boolean;\n}\n\nexport type ReadTextFile = (path: string) => Promise<string>;\n\nexport async function loadConfig(\n cwd: string,\n configPath: string | undefined,\n readTextFile: ReadTextFile,\n): Promise<{ engine: EngineConfig; ignore: string[] }> {\n const candidates = configPath\n ? [configPath]\n : [`${cwd.replace(/\\/$/, \"\")}/.hookogneserc.json`];\n\n let fileCfg: FileConfig = {};\n for (const c of candidates) {\n try {\n const text = await readTextFile(c);\n fileCfg = JSON.parse(text);\n break;\n } catch {\n // not found — fine, use defaults\n }\n }\n\n const rules: EngineConfig[\"rules\"] = { ...DEFAULT_RULES };\n if (fileCfg.rules) {\n for (const [id, spec] of Object.entries(fileCfg.rules)) {\n if (Array.isArray(spec)) {\n rules[id] = { severity: spec[0], options: spec[1] };\n } else {\n rules[id] = { severity: spec };\n }\n }\n }\n\n return {\n engine: {\n rules,\n cwd,\n typeAware: fileCfg.typeAware ?? false,\n },\n ignore: fileCfg.ignore ?? DEFAULT_IGNORE,\n };\n}\n\nexport function applyCliRuleOverrides(\n cfg: EngineConfig,\n overrides: Array<{ id: string; severity: Severity }>,\n): EngineConfig {\n const rules = { ...cfg.rules };\n for (const o of overrides) {\n rules[o.id] = {\n ...(rules[o.id] ?? { severity: \"off\" }),\n severity: o.severity,\n };\n }\n return { ...cfg, rules };\n}\n","import type { Formatter } from \"./types.ts\";\n\nexport const stylish: Formatter = (\n { diagnostics, filesScanned, durationMs },\n) => {\n if (diagnostics.length === 0) {\n return `✓ no problems found (${filesScanned} files, ${durationMs}ms)\\n`;\n }\n const byFile = new Map<string, typeof diagnostics>();\n for (const d of diagnostics) {\n if (!byFile.has(d.file)) byFile.set(d.file, []);\n byFile.get(d.file)!.push(d);\n }\n const lines: string[] = [];\n for (const [file, ds] of byFile) {\n lines.push(`\\n${file}`);\n for (const d of ds) {\n const sev = d.severity === \"error\" ? \"error\" : \"warn \";\n const loc = `${d.line}:${d.column}`.padEnd(7);\n lines.push(` ${loc} ${sev} ${d.message} ${d.rule}`);\n }\n }\n const errors = diagnostics.filter((d) => d.severity === \"error\").length;\n const warnings = diagnostics.filter((d) => d.severity === \"warn\").length;\n lines.push(\n `\\n${diagnostics.length} problems (${errors} error${\n errors === 1 ? \"\" : \"s\"\n }, ${warnings} warning${\n warnings === 1 ? \"\" : \"s\"\n }) in ${filesScanned} files, ${durationMs}ms`,\n );\n return lines.join(\"\\n\") + \"\\n\";\n};\n","import type { Formatter } from \"./types.ts\";\n\nexport const json: Formatter = (ctx) => JSON.stringify(ctx, null, 2);\n","import type { Formatter } from \"./types.ts\";\n\nexport const sarif: Formatter = ({ diagnostics }) => {\n const ruleIds = [...new Set(diagnostics.map((d) => d.rule))];\n return JSON.stringify(\n {\n version: \"2.1.0\",\n $schema:\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n runs: [{\n tool: {\n driver: {\n name: \"hook-o-gnese\",\n informationUri: \"https://github.com/rehoutm/spaghetti-hook-o-gnese\",\n rules: ruleIds.map((id) => ({ id })),\n },\n },\n results: diagnostics.map((d) => ({\n ruleId: d.rule,\n level: d.severity === \"error\" ? \"error\" : \"warning\",\n message: { text: d.message },\n locations: [{\n physicalLocation: {\n artifactLocation: { uri: d.file },\n region: {\n startLine: d.line,\n startColumn: d.column,\n endLine: d.endLine,\n endColumn: d.endColumn,\n },\n },\n }],\n })),\n }],\n },\n null,\n 2,\n );\n};\n","import type { Formatter } from \"./types.ts\";\n\nexport const github: Formatter = ({ diagnostics }) =>\n diagnostics.map((d) => {\n const cmd = d.severity === \"error\" ? \"::error\" : \"::warning\";\n const safe = d.message.replace(/\\r?\\n/g, \" \").replace(/::/g, \":\");\n return `${cmd} file=${d.file},line=${d.line},col=${d.column},title=${d.rule}::${safe}`;\n }).join(\"\\n\") + \"\\n\";\n","import { globby } from \"globby\";\nimport { lintFiles } from \"./engine.ts\";\nimport type { Severity } from \"./engine.ts\";\nimport { applyCliRuleOverrides, DEFAULT_IGNORE, loadConfig } from \"./config.ts\";\nimport { stylish } from \"./formatters/stylish.ts\";\nimport { json as jsonFmt } from \"./formatters/json.ts\";\nimport { sarif } from \"./formatters/sarif.ts\";\nimport { github } from \"./formatters/github.ts\";\nimport type { Formatter } from \"./formatters/types.ts\";\n\nconst FORMATTERS: Record<string, Formatter> = {\n stylish,\n json: jsonFmt,\n sarif,\n github,\n};\n\nexport const HELP = `\nhook-o-gnese — score React hook complexity\n\nUsage:\n hook-o-gnese [options] <paths...>\n\nOptions:\n --format=<fmt> stylish (default) | json | sarif | github\n --config=<path> path to .hookogneserc.json\n --type-aware enable custom-hook-depth (slower, uses TS Compiler API)\n --rule=<id>=<sev> override rule severity (off|warn|error). Repeatable.\n --help, -h show this message\n\nExamples:\n hook-o-gnese ./src\n hook-o-gnese ./src --format=sarif > report.sarif\n hook-o-gnese ./src --type-aware --rule=hook-o-gnese/state-scatter=error\n`.trim();\n\nexport interface CliOptions {\n paths: string[];\n format: string;\n config?: string;\n typeAware: boolean;\n ruleOverrides: Array<{ id: string; severity: Severity }>;\n cwd: string;\n}\n\nexport interface RuntimeIO {\n readTextFile(path: string): Promise<string>;\n writeStdout(s: string): void;\n writeStderr(s: string): void;\n}\n\nexport async function runCli(opts: CliOptions, io: RuntimeIO): Promise<number> {\n if (opts.paths.length === 0) {\n io.writeStderr(\"Error: no paths provided. Use --help for usage.\\n\");\n return 2;\n }\n\n const formatter = FORMATTERS[opts.format];\n if (!formatter) {\n io.writeStderr(`Error: unknown format '${opts.format}'\\n`);\n return 2;\n }\n\n const { engine, ignore } = await loadConfig(\n opts.cwd,\n opts.config,\n io.readTextFile,\n );\n if (opts.typeAware) engine.typeAware = true;\n const finalEngine = applyCliRuleOverrides(engine, opts.ruleOverrides);\n\n const files = await globby(opts.paths, {\n ignore: [...DEFAULT_IGNORE, ...ignore],\n expandDirectories: { extensions: [\"ts\", \"tsx\", \"js\", \"jsx\"] },\n absolute: false,\n });\n\n if (files.length === 0) {\n io.writeStderr(\"Error: no matching files found\\n\");\n return 2;\n }\n\n const start = performance.now();\n const diagnostics = await lintFiles(files, finalEngine, io.readTextFile);\n const durationMs = Math.round(performance.now() - start);\n\n io.writeStdout(formatter({\n diagnostics,\n filesScanned: files.length,\n durationMs,\n }));\n\n if (diagnostics.some((d) => d.severity === \"error\")) return 1;\n return 0;\n}\n","#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { readFile } from \"node:fs/promises\";\nimport { HELP, runCli } from \"./cli-core.ts\";\nimport type { Severity } from \"./engine.ts\";\n\nconst { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n help: { type: \"boolean\", short: \"h\" },\n \"type-aware\": { type: \"boolean\" },\n format: { type: \"string\", default: \"stylish\" },\n config: { type: \"string\" },\n rule: { type: \"string\", multiple: true },\n },\n allowPositionals: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst overrides = (values.rule ?? []).map((spec) => {\n const [id, sev] = spec.split(\"=\");\n return { id, severity: sev as Severity };\n});\n\nconst code = await runCli(\n {\n paths: positionals,\n format: values.format as string,\n config: values.config,\n typeAware: !!values[\"type-aware\"],\n ruleOverrides: overrides,\n cwd: process.cwd(),\n },\n {\n readTextFile: (p) => readFile(p, \"utf-8\"),\n writeStdout: (s) => {\n process.stdout.write(s);\n },\n writeStderr: (s) => {\n process.stderr.write(s);\n },\n },\n);\nprocess.exit(code);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["jsonFmt"],"sources":["../src/config.ts","../src/formatters/stylish.ts","../src/formatters/json.ts","../src/formatters/sarif.ts","../src/formatters/github.ts","../src/cli-core.ts","../src/cli.node.ts"],"sourcesContent":["import type { EngineConfig, Severity } from \"./engine.ts\";\n\nconst DEFAULT_RULES: Record<string, { severity: Severity; options?: unknown }> =\n {\n \"hook-o-gnese/no-fat-effects\": { severity: \"warn\" },\n \"hook-o-gnese/state-scatter\": { severity: \"warn\" },\n \"hook-o-gnese/hook-coupling\": { severity: \"error\" },\n \"hook-o-gnese/custom-hook-depth\": {\n severity: \"warn\",\n options: { maxDepth: 3 },\n },\n };\n\nexport const DEFAULT_IGNORE = [\n \"**/node_modules/**\",\n \"**/dist/**\",\n \"**/build/**\",\n \"**/.next/**\",\n \"**/.cache/**\",\n];\n\ninterface FileConfig {\n rules?: Record<string, Severity | [Severity, unknown]>;\n ignore?: string[];\n typeAware?: boolean;\n}\n\nexport type ReadTextFile = (path: string) => Promise<string>;\n\nexport async function loadConfig(\n cwd: string,\n configPath: string | undefined,\n readTextFile: ReadTextFile,\n): Promise<{ engine: EngineConfig; ignore: string[] }> {\n const candidates = configPath\n ? [configPath]\n : [`${cwd.replace(/\\/$/, \"\")}/.hookogneserc.json`];\n\n let fileCfg: FileConfig = {};\n for (const c of candidates) {\n try {\n const text = await readTextFile(c);\n fileCfg = JSON.parse(text);\n break;\n } catch {\n // not found — fine, use defaults\n }\n }\n\n const rules: EngineConfig[\"rules\"] = { ...DEFAULT_RULES };\n if (fileCfg.rules) {\n for (const [id, spec] of Object.entries(fileCfg.rules)) {\n if (Array.isArray(spec)) {\n rules[id] = { severity: spec[0], options: spec[1] };\n } else {\n rules[id] = { severity: spec };\n }\n }\n }\n\n return {\n engine: {\n rules,\n cwd,\n typeAware: fileCfg.typeAware ?? false,\n },\n ignore: fileCfg.ignore ?? DEFAULT_IGNORE,\n };\n}\n\nexport function applyCliRuleOverrides(\n cfg: EngineConfig,\n overrides: Array<{ id: string; severity: Severity }>,\n): EngineConfig {\n const rules = { ...cfg.rules };\n for (const o of overrides) {\n rules[o.id] = {\n ...(rules[o.id] ?? { severity: \"off\" }),\n severity: o.severity,\n };\n }\n return { ...cfg, rules };\n}\n","import type { Formatter } from \"./types.ts\";\n\nexport const stylish: Formatter = (\n { diagnostics, filesScanned, durationMs },\n) => {\n if (diagnostics.length === 0) {\n return `✓ no problems found (${filesScanned} files, ${durationMs}ms)\\n`;\n }\n const byFile = new Map<string, typeof diagnostics>();\n for (const d of diagnostics) {\n if (!byFile.has(d.file)) byFile.set(d.file, []);\n byFile.get(d.file)!.push(d);\n }\n const lines: string[] = [];\n for (const [file, ds] of byFile) {\n lines.push(`\\n${file}`);\n for (const d of ds) {\n const sev = d.severity === \"error\" ? \"error\" : \"warn \";\n const loc = `${d.line}:${d.column}`.padEnd(7);\n lines.push(` ${loc} ${sev} ${d.message} ${d.rule}`);\n }\n }\n const errors = diagnostics.filter((d) => d.severity === \"error\").length;\n const warnings = diagnostics.filter((d) => d.severity === \"warn\").length;\n lines.push(\n `\\n${diagnostics.length} problems (${errors} error${\n errors === 1 ? \"\" : \"s\"\n }, ${warnings} warning${\n warnings === 1 ? \"\" : \"s\"\n }) in ${filesScanned} files, ${durationMs}ms`,\n );\n return lines.join(\"\\n\") + \"\\n\";\n};\n","import type { Formatter } from \"./types.ts\";\n\nexport const json: Formatter = (ctx) => JSON.stringify(ctx, null, 2);\n","import type { Formatter } from \"./types.ts\";\n\nexport const sarif: Formatter = ({ diagnostics }) => {\n const ruleIds = [...new Set(diagnostics.map((d) => d.rule))];\n return JSON.stringify(\n {\n version: \"2.1.0\",\n $schema:\n \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json\",\n runs: [{\n tool: {\n driver: {\n name: \"hook-o-gnese\",\n informationUri: \"https://github.com/rehoutm/spaghetti-hook-o-gnese\",\n rules: ruleIds.map((id) => ({ id })),\n },\n },\n results: diagnostics.map((d) => ({\n ruleId: d.rule,\n level: d.severity === \"error\" ? \"error\" : \"warning\",\n message: { text: d.message },\n locations: [{\n physicalLocation: {\n artifactLocation: { uri: d.file },\n region: {\n startLine: d.line,\n startColumn: d.column,\n endLine: d.endLine,\n endColumn: d.endColumn,\n },\n },\n }],\n })),\n }],\n },\n null,\n 2,\n );\n};\n","import type { Formatter } from \"./types.ts\";\n\nexport const github: Formatter = ({ diagnostics }) =>\n diagnostics.map((d) => {\n const cmd = d.severity === \"error\" ? \"::error\" : \"::warning\";\n const safe = d.message.replace(/\\r?\\n/g, \" \").replace(/::/g, \":\");\n return `${cmd} file=${d.file},line=${d.line},col=${d.column},title=${d.rule}::${safe}`;\n }).join(\"\\n\") + \"\\n\";\n","import { globby } from \"globby\";\nimport { lintFiles } from \"./engine.ts\";\nimport type { Severity } from \"./engine.ts\";\nimport { applyCliRuleOverrides, DEFAULT_IGNORE, loadConfig } from \"./config.ts\";\nimport { isTypescriptAvailable } from \"./ts-program.ts\";\nimport { stylish } from \"./formatters/stylish.ts\";\nimport { json as jsonFmt } from \"./formatters/json.ts\";\nimport { sarif } from \"./formatters/sarif.ts\";\nimport { github } from \"./formatters/github.ts\";\nimport type { Formatter } from \"./formatters/types.ts\";\n\nconst FORMATTERS: Record<string, Formatter> = {\n stylish,\n json: jsonFmt,\n sarif,\n github,\n};\n\nexport const HELP = `\nhook-o-gnese — score React hook complexity\n\nUsage:\n hook-o-gnese [options] <paths...>\n\nOptions:\n --format=<fmt> stylish (default) | json | sarif | github\n --config=<path> path to .hookogneserc.json\n --type-aware enable custom-hook-depth (slower, uses TS Compiler API).\n Requires 'typescript' installed in your project.\n --rule=<id>=<sev> override rule severity (off|warn|error). Repeatable.\n --help, -h show this message\n\nExamples:\n hook-o-gnese ./src\n hook-o-gnese ./src --format=sarif > report.sarif\n hook-o-gnese ./src --type-aware --rule=hook-o-gnese/state-scatter=error\n`.trim();\n\nexport interface CliOptions {\n paths: string[];\n format: string;\n config?: string;\n typeAware: boolean;\n ruleOverrides: Array<{ id: string; severity: Severity }>;\n cwd: string;\n}\n\nexport interface RuntimeIO {\n readTextFile(path: string): Promise<string>;\n writeStdout(s: string): void;\n writeStderr(s: string): void;\n}\n\nexport async function runCli(opts: CliOptions, io: RuntimeIO): Promise<number> {\n if (opts.paths.length === 0) {\n io.writeStderr(\"Error: no paths provided. Use --help for usage.\\n\");\n return 2;\n }\n\n const formatter = FORMATTERS[opts.format];\n if (!formatter) {\n io.writeStderr(`Error: unknown format '${opts.format}'\\n`);\n return 2;\n }\n\n const { engine, ignore } = await loadConfig(\n opts.cwd,\n opts.config,\n io.readTextFile,\n );\n if (opts.typeAware) {\n if (isTypescriptAvailable(opts.cwd)) {\n engine.typeAware = true;\n } else {\n io.writeStderr(\n \"warning: --type-aware was requested but the 'typescript' package \" +\n \"is not installed. Skipping type-aware rules. \" +\n \"Install with: npm i -D typescript@>=6\\n\",\n );\n }\n }\n const finalEngine = applyCliRuleOverrides(engine, opts.ruleOverrides);\n\n const files = await globby(opts.paths, {\n ignore: [...DEFAULT_IGNORE, ...ignore],\n expandDirectories: { extensions: [\"ts\", \"tsx\", \"js\", \"jsx\"] },\n absolute: false,\n });\n\n if (files.length === 0) {\n io.writeStderr(\"Error: no matching files found\\n\");\n return 2;\n }\n\n const start = performance.now();\n const diagnostics = await lintFiles(files, finalEngine, io.readTextFile);\n const durationMs = Math.round(performance.now() - start);\n\n io.writeStdout(formatter({\n diagnostics,\n filesScanned: files.length,\n durationMs,\n }));\n\n if (diagnostics.some((d) => d.severity === \"error\")) return 1;\n return 0;\n}\n","#!/usr/bin/env node\nimport { parseArgs } from \"node:util\";\nimport { readFile } from \"node:fs/promises\";\nimport { HELP, runCli } from \"./cli-core.ts\";\nimport type { Severity } from \"./engine.ts\";\n\nconst { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n options: {\n help: { type: \"boolean\", short: \"h\" },\n \"type-aware\": { type: \"boolean\" },\n format: { type: \"string\", default: \"stylish\" },\n config: { type: \"string\" },\n rule: { type: \"string\", multiple: true },\n },\n allowPositionals: true,\n});\n\nif (values.help) {\n console.log(HELP);\n process.exit(0);\n}\n\nconst overrides = (values.rule ?? []).map((spec) => {\n const [id, sev] = spec.split(\"=\");\n return { id, severity: sev as Severity };\n});\n\nconst code = await runCli(\n {\n paths: positionals,\n format: values.format as string,\n config: values.config,\n typeAware: !!values[\"type-aware\"],\n ruleOverrides: overrides,\n cwd: process.cwd(),\n },\n {\n readTextFile: (p) => readFile(p, \"utf-8\"),\n writeStdout: (s) => {\n process.stdout.write(s);\n },\n writeStderr: (s) => {\n process.stderr.write(s);\n },\n },\n);\nprocess.exit(code);\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ALL_RULES } from "./registry-
|
|
1
|
+
import { t as ALL_RULES } from "./registry-YXM4yhf2.mjs";
|
|
2
2
|
import { parseSync } from "oxc-parser";
|
|
3
3
|
//#region src/engine.ts
|
|
4
4
|
const TYPE_AWARE_RULES = new Set(["hook-o-gnese/custom-hook-depth"]);
|
|
@@ -111,4 +111,4 @@ async function lintFiles(filePaths, config, readTextFile) {
|
|
|
111
111
|
//#endregion
|
|
112
112
|
export { lintFiles as n, lintFile as t };
|
|
113
113
|
|
|
114
|
-
//# sourceMappingURL=engine-
|
|
114
|
+
//# sourceMappingURL=engine-D5X2INMx.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine-
|
|
1
|
+
{"version":3,"file":"engine-D5X2INMx.mjs","names":[],"sources":["../src/engine.ts"],"sourcesContent":["import { parseSync } from \"oxc-parser\";\nimport { ALL_RULES } from \"./rules/registry.ts\";\n\nexport type Severity = \"off\" | \"warn\" | \"error\";\n\nexport interface Diagnostic {\n file: string;\n rule: string;\n severity: Exclude<Severity, \"off\">;\n message: string;\n line: number;\n column: number;\n endLine?: number;\n endColumn?: number;\n}\n\nexport interface RuleConfig {\n severity: Severity;\n options?: unknown;\n}\n\nexport interface EngineConfig {\n rules: Record<string, RuleConfig>;\n cwd: string;\n typeAware: boolean;\n}\n\nconst TYPE_AWARE_RULES = new Set([\"hook-o-gnese/custom-hook-depth\"]);\n\nfunction ruleNamespace(id: string): string {\n return id.replace(/^hook-o-gnese\\//, \"\");\n}\n\nfunction buildLineOffsets(source: string): number[] {\n const offsets = [0];\n for (let i = 0; i < source.length; i++) {\n if (source.charCodeAt(i) === 10) offsets.push(i + 1);\n }\n return offsets;\n}\n\nfunction offsetToLineCol(\n offset: number,\n lineOffsets: number[],\n): { line: number; column: number } {\n let lo = 0;\n let hi = lineOffsets.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >>> 1;\n if (lineOffsets[mid] <= offset) lo = mid;\n else hi = mid - 1;\n }\n return { line: lo + 1, column: offset - lineOffsets[lo] + 1 };\n}\n\nfunction getLoc(\n node: any,\n lineOffsets: number[],\n): { line: number; column: number; endLine?: number; endColumn?: number } {\n const startOffset = typeof node?.start === \"number\" ? node.start : undefined;\n const endOffset = typeof node?.end === \"number\" ? node.end : undefined;\n if (startOffset === undefined) {\n return { line: 1, column: 1 };\n }\n const start = offsetToLineCol(startOffset, lineOffsets);\n const end = endOffset !== undefined\n ? offsetToLineCol(endOffset, lineOffsets)\n : undefined;\n return {\n line: start.line,\n column: start.column,\n endLine: end?.line,\n endColumn: end?.column,\n };\n}\n\nfunction walkAST(node: any, handlers: Record<string, any>) {\n if (!node || typeof node !== \"object\") return;\n const enter = handlers[node.type];\n if (enter) enter(node);\n for (const key in node) {\n const v = node[key];\n if (Array.isArray(v)) {\n for (const c of v) walkAST(c, handlers);\n } else if (v && typeof v === \"object\") {\n walkAST(v, handlers);\n }\n }\n const exit = handlers[`${node.type}:exit`];\n if (exit) exit(node);\n}\n\nexport async function lintFile(\n filePath: string,\n source: string,\n config: EngineConfig,\n): Promise<Diagnostic[]> {\n const lang = filePath.endsWith(\".tsx\")\n ? \"tsx\"\n : filePath.endsWith(\".ts\")\n ? \"ts\"\n : filePath.endsWith(\".jsx\")\n ? \"jsx\"\n : \"js\";\n\n const parsed = parseSync(filePath, source, {\n lang,\n sourceType: \"module\",\n });\n\n const lineOffsets = buildLineOffsets(source);\n\n if (parsed.errors?.length) {\n return parsed.errors.map((e: any) => {\n const offset = typeof e.labels?.[0]?.start === \"number\"\n ? e.labels[0].start\n : undefined;\n const loc = offset !== undefined\n ? offsetToLineCol(offset, lineOffsets)\n : { line: 1, column: 1 };\n return {\n file: filePath,\n rule: \"parse-error\",\n severity: \"error\" as const,\n message: e.message ?? \"parse error\",\n line: loc.line,\n column: loc.column,\n };\n });\n }\n\n // Bail early on non-React files\n const imports = parsed.module?.staticImports ?? [];\n const hasReact = imports.some((i: any) =>\n (i.moduleRequest?.value ?? i.source?.value) === \"react\"\n );\n if (!hasReact) return [];\n\n const out: Diagnostic[] = [];\n\n for (const [ruleId, ruleCfg] of Object.entries(config.rules)) {\n if (ruleCfg.severity === \"off\") continue;\n if (!config.typeAware && TYPE_AWARE_RULES.has(ruleId)) continue;\n\n const rule = (ALL_RULES as any)[ruleNamespace(ruleId)];\n if (!rule) continue;\n\n const localDiags: Diagnostic[] = [];\n const context = {\n options: ruleCfg.options ? [ruleCfg.options] : [],\n filename: filePath,\n cwd: config.cwd,\n report(d: { message: string; node: any; severity?: \"warn\" | \"error\" }) {\n const loc = getLoc(d.node, lineOffsets);\n const cfgSev = ruleCfg.severity as \"warn\" | \"error\";\n // Rule-emitted severity only escalates (warn → error); never downgrades.\n const severity = d.severity === \"error\" ? \"error\" : cfgSev;\n localDiags.push({\n file: filePath,\n rule: ruleId,\n severity,\n message: d.message,\n ...loc,\n });\n },\n };\n\n const handlers = rule.create(context);\n walkAST(parsed.program, handlers);\n out.push(...localDiags);\n }\n\n return out;\n}\n\nexport type ReadTextFile = (path: string) => Promise<string>;\n\nexport async function lintFiles(\n filePaths: string[],\n config: EngineConfig,\n readTextFile: ReadTextFile,\n): Promise<Diagnostic[]> {\n const results = await Promise.all(\n filePaths.map(async (p) => {\n const src = await readTextFile(p);\n return lintFile(p, src, config);\n }),\n );\n return results.flat();\n}\n"],"mappings":";;;AA2BA,MAAM,mBAAmB,IAAI,IAAI,CAAC,iCAAiC,CAAC;AAEpE,SAAS,cAAc,IAAoB;AACzC,QAAO,GAAG,QAAQ,mBAAmB,GAAG;;AAG1C,SAAS,iBAAiB,QAA0B;CAClD,MAAM,UAAU,CAAC,EAAE;AACnB,MAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,IACjC,KAAI,OAAO,WAAW,EAAE,KAAK,GAAI,SAAQ,KAAK,IAAI,EAAE;AAEtD,QAAO;;AAGT,SAAS,gBACP,QACA,aACkC;CAClC,IAAI,KAAK;CACT,IAAI,KAAK,YAAY,SAAS;AAC9B,QAAO,KAAK,IAAI;EACd,MAAM,MAAO,KAAK,KAAK,MAAO;AAC9B,MAAI,YAAY,QAAQ,OAAQ,MAAK;MAChC,MAAK,MAAM;;AAElB,QAAO;EAAE,MAAM,KAAK;EAAG,QAAQ,SAAS,YAAY,MAAM;EAAG;;AAG/D,SAAS,OACP,MACA,aACwE;CACxE,MAAM,cAAc,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ,KAAA;CACnE,MAAM,YAAY,OAAO,MAAM,QAAQ,WAAW,KAAK,MAAM,KAAA;AAC7D,KAAI,gBAAgB,KAAA,EAClB,QAAO;EAAE,MAAM;EAAG,QAAQ;EAAG;CAE/B,MAAM,QAAQ,gBAAgB,aAAa,YAAY;CACvD,MAAM,MAAM,cAAc,KAAA,IACtB,gBAAgB,WAAW,YAAY,GACvC,KAAA;AACJ,QAAO;EACL,MAAM,MAAM;EACZ,QAAQ,MAAM;EACd,SAAS,KAAK;EACd,WAAW,KAAK;EACjB;;AAGH,SAAS,QAAQ,MAAW,UAA+B;AACzD,KAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;CACvC,MAAM,QAAQ,SAAS,KAAK;AAC5B,KAAI,MAAO,OAAM,KAAK;AACtB,MAAK,MAAM,OAAO,MAAM;EACtB,MAAM,IAAI,KAAK;AACf,MAAI,MAAM,QAAQ,EAAE,CAClB,MAAK,MAAM,KAAK,EAAG,SAAQ,GAAG,SAAS;WAC9B,KAAK,OAAO,MAAM,SAC3B,SAAQ,GAAG,SAAS;;CAGxB,MAAM,OAAO,SAAS,GAAG,KAAK,KAAK;AACnC,KAAI,KAAM,MAAK,KAAK;;AAGtB,eAAsB,SACpB,UACA,QACA,QACuB;CASvB,MAAM,SAAS,UAAU,UAAU,QAAQ;EACzC,MATW,SAAS,SAAS,OAAO,GAClC,QACA,SAAS,SAAS,MAAM,GACxB,OACA,SAAS,SAAS,OAAO,GACzB,QACA;EAIF,YAAY;EACb,CAAC;CAEF,MAAM,cAAc,iBAAiB,OAAO;AAE5C,KAAI,OAAO,QAAQ,OACjB,QAAO,OAAO,OAAO,KAAK,MAAW;EACnC,MAAM,SAAS,OAAO,EAAE,SAAS,IAAI,UAAU,WAC3C,EAAE,OAAO,GAAG,QACZ,KAAA;EACJ,MAAM,MAAM,WAAW,KAAA,IACnB,gBAAgB,QAAQ,YAAY,GACpC;GAAE,MAAM;GAAG,QAAQ;GAAG;AAC1B,SAAO;GACL,MAAM;GACN,MAAM;GACN,UAAU;GACV,SAAS,EAAE,WAAW;GACtB,MAAM,IAAI;GACV,QAAQ,IAAI;GACb;GACD;AAQJ,KAAI,EAJY,OAAO,QAAQ,iBAAiB,EAAE,EACzB,MAAM,OAC5B,EAAE,eAAe,SAAS,EAAE,QAAQ,WAAW,QAErC,CAAE,QAAO,EAAE;CAExB,MAAM,MAAoB,EAAE;AAE5B,MAAK,MAAM,CAAC,QAAQ,YAAY,OAAO,QAAQ,OAAO,MAAM,EAAE;AAC5D,MAAI,QAAQ,aAAa,MAAO;AAChC,MAAI,CAAC,OAAO,aAAa,iBAAiB,IAAI,OAAO,CAAE;EAEvD,MAAM,OAAQ,UAAkB,cAAc,OAAO;AACrD,MAAI,CAAC,KAAM;EAEX,MAAM,aAA2B,EAAE;EACnC,MAAM,UAAU;GACd,SAAS,QAAQ,UAAU,CAAC,QAAQ,QAAQ,GAAG,EAAE;GACjD,UAAU;GACV,KAAK,OAAO;GACZ,OAAO,GAAgE;IACrE,MAAM,MAAM,OAAO,EAAE,MAAM,YAAY;IACvC,MAAM,SAAS,QAAQ;IAEvB,MAAM,WAAW,EAAE,aAAa,UAAU,UAAU;AACpD,eAAW,KAAK;KACd,MAAM;KACN,MAAM;KACN;KACA,SAAS,EAAE;KACX,GAAG;KACJ,CAAC;;GAEL;EAED,MAAM,WAAW,KAAK,OAAO,QAAQ;AACrC,UAAQ,OAAO,SAAS,SAAS;AACjC,MAAI,KAAK,GAAG,WAAW;;AAGzB,QAAO;;AAKT,eAAsB,UACpB,WACA,QACA,cACuB;AAOvB,SAAO,MANe,QAAQ,IAC5B,UAAU,IAAI,OAAO,MAAM;AAEzB,SAAO,SAAS,GAAG,MADD,aAAa,EAAE,EACT,OAAO;GAC/B,CACH,EACc,MAAM"}
|
package/dist/engine.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as lintFiles, t as lintFile } from "./engine-
|
|
1
|
+
import { n as lintFiles, t as lintFile } from "./engine-D5X2INMx.mjs";
|
|
2
2
|
export { lintFile, lintFiles };
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
//#region src/ast-helpers.ts
|
|
3
4
|
const HOOK_RE = /^use[A-Z]/;
|
|
4
5
|
function getHookName(node) {
|
|
@@ -315,14 +316,41 @@ const hookCoupling = {
|
|
|
315
316
|
};
|
|
316
317
|
//#endregion
|
|
317
318
|
//#region src/ts-program.ts
|
|
319
|
+
const tsCache = /* @__PURE__ */ new Map();
|
|
320
|
+
function requireFromCwd(cwd) {
|
|
321
|
+
return createRequire(join(cwd, "_"));
|
|
322
|
+
}
|
|
323
|
+
function loadTs(cwd) {
|
|
324
|
+
const cached = tsCache.get(cwd);
|
|
325
|
+
if (cached) return cached;
|
|
326
|
+
try {
|
|
327
|
+
const mod = requireFromCwd(cwd)("typescript");
|
|
328
|
+
const lib = mod.default ?? mod;
|
|
329
|
+
tsCache.set(cwd, lib);
|
|
330
|
+
return lib;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
throw new Error("hook-o-gnese: --type-aware requires the 'typescript' package to be installed in your project. Install it with: npm i -D typescript@>=6", { cause: err });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function isTypescriptAvailable(cwd) {
|
|
336
|
+
try {
|
|
337
|
+
loadTs(cwd);
|
|
338
|
+
return true;
|
|
339
|
+
} catch {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
318
343
|
var TsProgramCache = class {
|
|
319
344
|
program = null;
|
|
320
345
|
rootDir;
|
|
346
|
+
ts;
|
|
321
347
|
constructor(rootDir) {
|
|
322
348
|
this.rootDir = rootDir;
|
|
349
|
+
this.ts = loadTs(rootDir);
|
|
323
350
|
}
|
|
324
351
|
getProgram() {
|
|
325
352
|
if (this.program) return this.program;
|
|
353
|
+
const ts = this.ts;
|
|
326
354
|
const configPath = ts.findConfigFile(this.rootDir, ts.sys.fileExists, "tsconfig.json");
|
|
327
355
|
let compilerOptions = {
|
|
328
356
|
target: ts.ScriptTarget.ESNext,
|
|
@@ -349,6 +377,7 @@ var TsProgramCache = class {
|
|
|
349
377
|
return this.program;
|
|
350
378
|
}
|
|
351
379
|
resolveIdentifierDeclaration(filePath, identifier) {
|
|
380
|
+
const ts = this.ts;
|
|
352
381
|
const program = this.getProgram();
|
|
353
382
|
const checker = program.getTypeChecker();
|
|
354
383
|
const absolute = filePath.startsWith("/") ? filePath : `${this.rootDir.replace(/\/$/, "")}/${filePath}`;
|
|
@@ -372,6 +401,7 @@ var TsProgramCache = class {
|
|
|
372
401
|
countTransitiveHookCalls(decl, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
373
402
|
if (depth > 10 || seen.has(decl)) return depth;
|
|
374
403
|
seen.add(decl);
|
|
404
|
+
const ts = this.ts;
|
|
375
405
|
const checker = this.getProgram().getTypeChecker();
|
|
376
406
|
let maxDepth = depth;
|
|
377
407
|
const visit = (node) => {
|
|
@@ -455,6 +485,6 @@ const ALL_RULES = {
|
|
|
455
485
|
}
|
|
456
486
|
};
|
|
457
487
|
//#endregion
|
|
458
|
-
export { ALL_RULES as t };
|
|
488
|
+
export { isTypescriptAvailable as n, ALL_RULES as t };
|
|
459
489
|
|
|
460
|
-
//# sourceMappingURL=registry-
|
|
490
|
+
//# sourceMappingURL=registry-YXM4yhf2.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-YXM4yhf2.mjs","names":["joinPath"],"sources":["../src/ast-helpers.ts","../src/scoring/effect-score.ts","../src/scoring/thresholds.ts","../src/rules/no-fat-effects.ts","../src/scoring/state-score.ts","../src/rules/state-scatter.ts","../src/scoring/coupling-score.ts","../src/rules/hook-coupling.ts","../src/ts-program.ts","../src/rules/custom-hook-depth.ts","../src/rules/registry.ts"],"sourcesContent":["type Node = { type: string; [k: string]: unknown };\n\nconst HOOK_RE = /^use[A-Z]/;\n\nexport function getHookName(node: Node): string | null {\n if (node.type !== \"CallExpression\") return null;\n const callee = (node as any).callee as Node;\n if (callee?.type !== \"Identifier\") return null;\n const name = (callee as any).name as string;\n return HOOK_RE.test(name) ? name : null;\n}\n\nexport function isHookCall(node: Node, expected: string): boolean {\n return getHookName(node) === expected;\n}\n\nexport function isReactComponent(node: Node): boolean {\n if (node.type === \"FunctionDeclaration\") {\n const name = (node as any).id?.name as string | undefined;\n if (!name || !/^[A-Z]/.test(name)) return false;\n return findReturnsJSX(node);\n }\n if (node.type === \"VariableDeclaration\") {\n const decl = (node as any).declarations?.[0];\n const name = decl?.id?.name as string | undefined;\n const init = decl?.init as Node | undefined;\n if (!name || !/^[A-Z]/.test(name) || !init) return false;\n if (\n init.type === \"ArrowFunctionExpression\" ||\n init.type === \"FunctionExpression\"\n ) {\n return findReturnsJSX(init);\n }\n }\n return false;\n}\n\nexport function findReturnsJSX(node: Node): boolean {\n let found = false;\n walk(node, (n) => {\n if (\n n.type === \"JSXElement\" ||\n n.type === \"JSXFragment\" ||\n n.type === \"JSXSelfClosingElement\"\n ) {\n found = true;\n return false;\n }\n return true;\n });\n return found;\n}\n\nexport function walk(\n node: Node,\n visit: (n: Node) => boolean | void,\n seen: WeakSet<Node> = new WeakSet(),\n): void {\n if (seen.has(node)) return;\n seen.add(node);\n const cont = visit(node);\n if (cont === false) return;\n for (const key in node) {\n if (key === \"parent\") continue;\n const val = (node as any)[key];\n if (Array.isArray(val)) {\n for (const child of val) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n walk(child as Node, visit, seen);\n }\n }\n } else if (val && typeof val === \"object\" && \"type\" in val) {\n walk(val as Node, visit, seen);\n }\n }\n}\n","import { walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface EffectScore {\n deps: number;\n branches: number;\n setStateCount: number;\n nestedEffects: number;\n hasCleanup: boolean;\n hasSubscriptionLike: boolean;\n total: number;\n}\n\nconst SET_STATE_RE = /^set[A-Z]/;\nconst BRANCH_TYPES = new Set([\n \"IfStatement\",\n \"ConditionalExpression\",\n \"SwitchCase\",\n \"LogicalExpression\",\n]);\n\nexport function scoreEffect(node: Node): EffectScore {\n const args = (node as any).arguments as Node[];\n const fn = args?.[0] as Node | undefined;\n const depsArr = args?.[1] as any;\n\n const deps = Array.isArray(depsArr?.elements) ? depsArr.elements.length : 0;\n let branches = 0;\n let setStateCount = 0;\n let nestedEffects = 0;\n let hasCleanup = false;\n let hasSubscriptionLike = false;\n\n if (fn) {\n const body = (fn as any).body as Node;\n if (body?.type === \"BlockStatement\") {\n for (const stmt of (body as any).body as Node[]) {\n if (stmt.type === \"ReturnStatement\") {\n const arg = (stmt as any).argument as Node | undefined;\n if (\n arg &&\n (arg.type === \"ArrowFunctionExpression\" ||\n arg.type === \"FunctionExpression\")\n ) hasCleanup = true;\n }\n }\n }\n\n walk(fn, (n) => {\n if (BRANCH_TYPES.has(n.type)) branches++;\n if (n.type === \"CallExpression\") {\n const callee = (n as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (SET_STATE_RE.test(name)) setStateCount++;\n if (name === \"useEffect\" && n !== node) nestedEffects++;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"setInterval\" ||\n name === \"setTimeout\"\n ) hasSubscriptionLike = true;\n }\n if (callee?.type === \"MemberExpression\") {\n const prop = (callee as any).property as Node;\n if (prop?.type === \"Identifier\") {\n const name = (prop as any).name as string;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"on\"\n ) hasSubscriptionLike = true;\n }\n }\n }\n return true;\n });\n }\n\n const cleanupPenalty = hasSubscriptionLike && !hasCleanup ? 3 : 0;\n const total = deps + branches * 2 + setStateCount * 1.5 +\n nestedEffects * 5 + cleanupPenalty;\n\n return {\n deps,\n branches,\n setStateCount,\n nestedEffects,\n hasCleanup,\n hasSubscriptionLike,\n total,\n };\n}\n","export interface Thresholds {\n fatEffect: { warn: number; error: number };\n stateScatter: { warn: number; error: number };\n hookCoupling: { warn: number; error: number };\n customHookDepth: { warn: number; error: number };\n}\n\nexport const DEFAULT_THRESHOLDS: Thresholds = {\n fatEffect: { warn: 10, error: 20 },\n stateScatter: { warn: 5, error: 8 },\n hookCoupling: { warn: 3, error: 6 },\n customHookDepth: { warn: 3, error: 5 },\n};\n","import { scoreEffect } from \"../scoring/effect-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isHookCall } from \"../ast-helpers.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport interface RuleContext {\n options: unknown[];\n filename?: string;\n cwd?: string;\n report: (d: {\n message: string;\n node: unknown;\n severity?: \"warn\" | \"error\";\n }) => void;\n}\n\nexport const noFatEffects = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag dense useEffect blocks\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.fatEffect.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.fatEffect.error;\n return {\n CallExpression(node: any) {\n if (!isHookCall(node, \"useEffect\")) return;\n const score = scoreEffect(node);\n if (score.total >= threshold) {\n const breakdown = `deps=${score.deps} branches=${score.branches} ` +\n `setStates=${score.setStateCount} nested=${score.nestedEffects}` +\n (score.hasSubscriptionLike && !score.hasCleanup\n ? \" missing-cleanup\"\n : \"\");\n const severity = score.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message: `useEffect entropy ${\n score.total.toFixed(1)\n } ≥ ${threshold} (${breakdown})`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface StateScore {\n useStateCount: number;\n correlatedSetters: number;\n total: number;\n}\n\nexport function scoreComponentState(componentNode: Node): StateScore {\n const setterNames = new Set<string>();\n let useStateCount = 0;\n\n // First pass: collect useState setters\n walk(componentNode, (n) => {\n if (!n) return true;\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n useStateCount++;\n const els = (id as any).elements as Node[];\n const setter = els?.[1];\n if (setter?.type === \"Identifier\") {\n setterNames.add((setter as any).name as string);\n }\n }\n }\n return true;\n });\n\n // Second pass: count correlated setters\n let correlatedSetters = 0;\n walk(componentNode, (n) => {\n if (!n) return true;\n if (\n n.type === \"FunctionDeclaration\" ||\n n.type === \"FunctionExpression\" ||\n n.type === \"ArrowFunctionExpression\"\n ) {\n const calledSetters = new Set<string>();\n walk(n, (m) => {\n if (!m) return true;\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (setterNames.has(name)) calledSetters.add(name);\n }\n }\n return true;\n });\n if (calledSetters.size >= 2) correlatedSetters += calledSetters.size;\n }\n return true;\n });\n\n const total = useStateCount + correlatedSetters * 0.5;\n return { useStateCount, correlatedSetters, total };\n}\n","import { scoreComponentState } from \"../scoring/state-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport const stateScatter = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag components with too many useState calls\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.stateScatter.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.stateScatter.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreComponentState(node);\n if (s.total >= threshold) {\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message:\n `state scatter ${s.total} ≥ ${threshold} (useStates=${s.useStateCount}, correlated setters=${s.correlatedSetters}). Consider useReducer.`,\n node,\n severity,\n });\n }\n }\n return {\n FunctionDeclaration: check,\n VariableDeclaration: check,\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface CouplingScore {\n total: number;\n readWriteSame: Array<{ state: string; effect: Node }>;\n}\n\nexport function scoreCoupling(componentNode: Node): CouplingScore {\n const stateBySetter = new Map<string, string>();\n walk(componentNode, (n) => {\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n const els = (id as any).elements as Node[];\n const stateId = els?.[0];\n const setterId = els?.[1];\n if (stateId?.type === \"Identifier\" && setterId?.type === \"Identifier\") {\n stateBySetter.set(\n (setterId as any).name as string,\n (stateId as any).name as string,\n );\n }\n }\n }\n return true;\n });\n\n const readWriteSame: Array<{ state: string; effect: Node }> = [];\n let total = 0;\n\n walk(componentNode, (n) => {\n if (n.type === \"CallExpression\" && isHookCall(n, \"useEffect\")) {\n const effectFn = ((n as any).arguments as Node[])?.[0];\n if (!effectFn) return true;\n\n const stateRefs = new Set<string>();\n const stateWrites = new Set<string>();\n const stateNames = new Set(stateBySetter.values());\n\n walk(effectFn, (m) => {\n if (m.type === \"Identifier\") {\n const name = (m as any).name as string;\n if (stateNames.has(name)) stateRefs.add(name);\n }\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const setter = (callee as any).name as string;\n const stateName = stateBySetter.get(setter);\n if (stateName) stateWrites.add(stateName);\n }\n }\n return true;\n });\n\n for (const written of stateWrites) {\n if (stateRefs.has(written)) {\n readWriteSame.push({ state: written, effect: n });\n total += 3;\n }\n }\n }\n return true;\n });\n\n return { total, readWriteSame };\n}\n","import { scoreCoupling } from \"../scoring/coupling-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport const hookCoupling = {\n meta: {\n type: \"problem\" as const,\n docs: {\n description: \"Flag effects that read state they also write (loop bait)\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.hookCoupling.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.hookCoupling.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreCoupling(node);\n if (s.total < threshold) return;\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n for (const v of s.readWriteSame) {\n context.report({\n message:\n `useEffect reads + writes same state '${v.state}' (loop risk)`,\n node: v.effect,\n severity,\n });\n }\n }\n return { FunctionDeclaration: check, VariableDeclaration: check };\n },\n};\n","import { createRequire } from \"node:module\";\nimport { join as joinPath } from \"node:path\";\nimport type ts from \"typescript\";\n\ntype TS = typeof ts;\n\n// Per-cwd cache: different consumers may resolve to different `typescript`\n// installs, so the cache key must include cwd.\nconst tsCache = new Map<string, TS>();\n\n// Resolve from a fictitious file inside the consumer's CWD so Node walks UP\n// their project tree (finding their installed `typescript`), not ours. This\n// matters for `npx`, where the package lives in npm's npx cache.\nfunction requireFromCwd(cwd: string) {\n return createRequire(joinPath(cwd, \"_\"));\n}\n\nfunction loadTs(cwd: string): TS {\n const cached = tsCache.get(cwd);\n if (cached) return cached;\n try {\n const mod = requireFromCwd(cwd)(\"typescript\") as TS | { default: TS };\n const lib = ((mod as { default?: TS }).default ?? mod) as TS;\n tsCache.set(cwd, lib);\n return lib;\n } catch (err) {\n throw new Error(\n \"hook-o-gnese: --type-aware requires the 'typescript' package to be \" +\n \"installed in your project. Install it with: npm i -D typescript@>=6\",\n { cause: err as Error },\n );\n }\n}\n\nexport function isTypescriptAvailable(cwd: string): boolean {\n try {\n loadTs(cwd);\n return true;\n } catch {\n return false;\n }\n}\n\nexport class TsProgramCache {\n private program: ts.Program | null = null;\n private rootDir: string;\n private ts: TS;\n\n constructor(rootDir: string) {\n this.rootDir = rootDir;\n this.ts = loadTs(rootDir);\n }\n\n private getProgram(): ts.Program {\n if (this.program) return this.program;\n const ts = this.ts;\n const configPath = ts.findConfigFile(\n this.rootDir,\n ts.sys.fileExists,\n \"tsconfig.json\",\n );\n let compilerOptions: ts.CompilerOptions = {\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n jsx: ts.JsxEmit.Preserve,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n allowJs: true,\n noEmit: true,\n skipLibCheck: true,\n strict: false,\n };\n let fileNames: string[] = [];\n if (configPath) {\n const cfg = ts.readConfigFile(configPath, ts.sys.readFile);\n const configDir = configPath.slice(0, configPath.lastIndexOf(\"/\"));\n const parsed = ts.parseJsonConfigFileContent(\n cfg.config,\n ts.sys,\n configDir,\n );\n compilerOptions = { ...compilerOptions, ...parsed.options };\n fileNames = parsed.fileNames;\n }\n this.program = ts.createProgram(fileNames, compilerOptions);\n return this.program;\n }\n\n resolveIdentifierDeclaration(\n filePath: string,\n identifier: string,\n ): ts.Declaration | null {\n const ts = this.ts;\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n const absolute = filePath.startsWith(\"/\")\n ? filePath\n : `${this.rootDir.replace(/\\/$/, \"\")}/${filePath}`;\n const sourceFile = program.getSourceFile(absolute) ??\n program.getSourceFile(filePath);\n if (!sourceFile) return null;\n\n let target: ts.Node | null = null;\n function find(node: ts.Node) {\n if (target) return;\n if (ts.isIdentifier(node) && node.text === identifier) {\n target = node;\n return;\n }\n ts.forEachChild(node, find);\n }\n find(sourceFile);\n if (!target) return null;\n\n const symbol = checker.getSymbolAtLocation(target);\n if (!symbol) return null;\n const aliased = symbol.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(symbol)\n : symbol;\n return aliased.declarations?.[0] ?? null;\n }\n\n countTransitiveHookCalls(\n decl: ts.Declaration,\n depth = 0,\n seen = new Set<ts.Declaration>(),\n ): number {\n if (depth > 10 || seen.has(decl)) return depth;\n seen.add(decl);\n const ts = this.ts;\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n let maxDepth = depth;\n\n const visit = (node: ts.Node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n /^use[A-Z]/.test(node.expression.text)\n ) {\n const sym = checker.getSymbolAtLocation(node.expression);\n if (sym) {\n const aliased = sym.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(sym)\n : sym;\n const innerDecl = aliased.declarations?.[0];\n if (innerDecl) {\n const sf = innerDecl.getSourceFile();\n if (\n sf.fileName.includes(\"node_modules/@types/react\") ||\n sf.fileName.includes(\"node_modules/react/\")\n ) {\n maxDepth = Math.max(maxDepth, depth + 1);\n } else {\n const childDepth = this.countTransitiveHookCalls(\n innerDecl,\n depth + 1,\n seen,\n );\n maxDepth = Math.max(maxDepth, childDepth);\n }\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n\n visit(decl);\n return maxDepth;\n }\n}\n","import { TsProgramCache } from \"../ts-program.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { getHookName } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n maxDepth?: number;\n errorMaxDepth?: number;\n}\n\nconst REACT_HOOKS = new Set([\n \"useState\",\n \"useEffect\",\n \"useLayoutEffect\",\n \"useMemo\",\n \"useCallback\",\n \"useReducer\",\n \"useContext\",\n \"useRef\",\n \"useImperativeHandle\",\n \"useDebugValue\",\n \"useId\",\n \"useTransition\",\n \"useDeferredValue\",\n \"useSyncExternalStore\",\n \"useInsertionEffect\",\n]);\n\nlet sharedCache: TsProgramCache | null = null;\n\nexport const customHookDepth = {\n meta: {\n type: \"suggestion\" as const,\n docs: {\n description:\n \"Flag custom hooks whose transitive nesting exceeds maxDepth (type-aware).\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const maxDepth = opts.maxDepth ?? DEFAULT_THRESHOLDS.customHookDepth.warn;\n const errorMaxDepth = opts.errorMaxDepth ??\n DEFAULT_THRESHOLDS.customHookDepth.error;\n const g = globalThis as {\n Deno?: { cwd(): string };\n process?: { cwd(): string };\n };\n const cwd = context.cwd ?? g.process?.cwd() ?? g.Deno?.cwd() ?? \".\";\n sharedCache ??= new TsProgramCache(cwd);\n const cache = sharedCache;\n const filename = context.filename;\n\n return {\n CallExpression(node: any) {\n const name = getHookName(node);\n if (!name || REACT_HOOKS.has(name)) return;\n if (!filename) return;\n const decl = cache.resolveIdentifierDeclaration(filename, name);\n if (!decl) return;\n const depth = cache.countTransitiveHookCalls(decl);\n if (depth >= maxDepth) {\n const severity = depth >= errorMaxDepth ? \"error\" : \"warn\";\n context.report({\n message:\n `custom hook '${name}' transitive depth ${depth} ≥ ${maxDepth}`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import type { Rule } from \"@oxlint/plugins\";\nimport { noFatEffects } from \"./no-fat-effects.ts\";\nimport { stateScatter } from \"./state-scatter.ts\";\nimport { hookCoupling } from \"./hook-coupling.ts\";\nimport { customHookDepth } from \"./custom-hook-depth.ts\";\n\n// Rules use our internal ESLint-compatible RuleContext shape; eslintCompatPlugin\n// adapts them to oxlint's stricter Rule contract at runtime.\nexport const ALL_RULES: Record<string, Rule> = {\n \"no-fat-effects\": noFatEffects as unknown as Rule,\n \"state-scatter\": stateScatter as unknown as Rule,\n \"hook-coupling\": hookCoupling as unknown as Rule,\n \"custom-hook-depth\": customHookDepth as unknown as Rule,\n};\n\nexport type RuleId =\n | \"no-fat-effects\"\n | \"state-scatter\"\n | \"hook-coupling\"\n | \"custom-hook-depth\";\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hook-o-gnese",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Score React hook complexity. Runs as oxlint plugin or standalone CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -52,6 +52,9 @@
|
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"oxlint": {
|
|
54
54
|
"optional": true
|
|
55
|
+
},
|
|
56
|
+
"typescript": {
|
|
57
|
+
"optional": true
|
|
55
58
|
}
|
|
56
59
|
},
|
|
57
60
|
"dependencies": {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"registry-iRG6wil9.mjs","names":[],"sources":["../src/ast-helpers.ts","../src/scoring/effect-score.ts","../src/scoring/thresholds.ts","../src/rules/no-fat-effects.ts","../src/scoring/state-score.ts","../src/rules/state-scatter.ts","../src/scoring/coupling-score.ts","../src/rules/hook-coupling.ts","../src/ts-program.ts","../src/rules/custom-hook-depth.ts","../src/rules/registry.ts"],"sourcesContent":["type Node = { type: string; [k: string]: unknown };\n\nconst HOOK_RE = /^use[A-Z]/;\n\nexport function getHookName(node: Node): string | null {\n if (node.type !== \"CallExpression\") return null;\n const callee = (node as any).callee as Node;\n if (callee?.type !== \"Identifier\") return null;\n const name = (callee as any).name as string;\n return HOOK_RE.test(name) ? name : null;\n}\n\nexport function isHookCall(node: Node, expected: string): boolean {\n return getHookName(node) === expected;\n}\n\nexport function isReactComponent(node: Node): boolean {\n if (node.type === \"FunctionDeclaration\") {\n const name = (node as any).id?.name as string | undefined;\n if (!name || !/^[A-Z]/.test(name)) return false;\n return findReturnsJSX(node);\n }\n if (node.type === \"VariableDeclaration\") {\n const decl = (node as any).declarations?.[0];\n const name = decl?.id?.name as string | undefined;\n const init = decl?.init as Node | undefined;\n if (!name || !/^[A-Z]/.test(name) || !init) return false;\n if (\n init.type === \"ArrowFunctionExpression\" ||\n init.type === \"FunctionExpression\"\n ) {\n return findReturnsJSX(init);\n }\n }\n return false;\n}\n\nexport function findReturnsJSX(node: Node): boolean {\n let found = false;\n walk(node, (n) => {\n if (\n n.type === \"JSXElement\" ||\n n.type === \"JSXFragment\" ||\n n.type === \"JSXSelfClosingElement\"\n ) {\n found = true;\n return false;\n }\n return true;\n });\n return found;\n}\n\nexport function walk(\n node: Node,\n visit: (n: Node) => boolean | void,\n seen: WeakSet<Node> = new WeakSet(),\n): void {\n if (seen.has(node)) return;\n seen.add(node);\n const cont = visit(node);\n if (cont === false) return;\n for (const key in node) {\n if (key === \"parent\") continue;\n const val = (node as any)[key];\n if (Array.isArray(val)) {\n for (const child of val) {\n if (child && typeof child === \"object\" && \"type\" in child) {\n walk(child as Node, visit, seen);\n }\n }\n } else if (val && typeof val === \"object\" && \"type\" in val) {\n walk(val as Node, visit, seen);\n }\n }\n}\n","import { walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface EffectScore {\n deps: number;\n branches: number;\n setStateCount: number;\n nestedEffects: number;\n hasCleanup: boolean;\n hasSubscriptionLike: boolean;\n total: number;\n}\n\nconst SET_STATE_RE = /^set[A-Z]/;\nconst BRANCH_TYPES = new Set([\n \"IfStatement\",\n \"ConditionalExpression\",\n \"SwitchCase\",\n \"LogicalExpression\",\n]);\n\nexport function scoreEffect(node: Node): EffectScore {\n const args = (node as any).arguments as Node[];\n const fn = args?.[0] as Node | undefined;\n const depsArr = args?.[1] as any;\n\n const deps = Array.isArray(depsArr?.elements) ? depsArr.elements.length : 0;\n let branches = 0;\n let setStateCount = 0;\n let nestedEffects = 0;\n let hasCleanup = false;\n let hasSubscriptionLike = false;\n\n if (fn) {\n const body = (fn as any).body as Node;\n if (body?.type === \"BlockStatement\") {\n for (const stmt of (body as any).body as Node[]) {\n if (stmt.type === \"ReturnStatement\") {\n const arg = (stmt as any).argument as Node | undefined;\n if (\n arg &&\n (arg.type === \"ArrowFunctionExpression\" ||\n arg.type === \"FunctionExpression\")\n ) hasCleanup = true;\n }\n }\n }\n\n walk(fn, (n) => {\n if (BRANCH_TYPES.has(n.type)) branches++;\n if (n.type === \"CallExpression\") {\n const callee = (n as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (SET_STATE_RE.test(name)) setStateCount++;\n if (name === \"useEffect\" && n !== node) nestedEffects++;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"setInterval\" ||\n name === \"setTimeout\"\n ) hasSubscriptionLike = true;\n }\n if (callee?.type === \"MemberExpression\") {\n const prop = (callee as any).property as Node;\n if (prop?.type === \"Identifier\") {\n const name = (prop as any).name as string;\n if (\n name === \"addEventListener\" ||\n name === \"subscribe\" ||\n name === \"on\"\n ) hasSubscriptionLike = true;\n }\n }\n }\n return true;\n });\n }\n\n const cleanupPenalty = hasSubscriptionLike && !hasCleanup ? 3 : 0;\n const total = deps + branches * 2 + setStateCount * 1.5 +\n nestedEffects * 5 + cleanupPenalty;\n\n return {\n deps,\n branches,\n setStateCount,\n nestedEffects,\n hasCleanup,\n hasSubscriptionLike,\n total,\n };\n}\n","export interface Thresholds {\n fatEffect: { warn: number; error: number };\n stateScatter: { warn: number; error: number };\n hookCoupling: { warn: number; error: number };\n customHookDepth: { warn: number; error: number };\n}\n\nexport const DEFAULT_THRESHOLDS: Thresholds = {\n fatEffect: { warn: 10, error: 20 },\n stateScatter: { warn: 5, error: 8 },\n hookCoupling: { warn: 3, error: 6 },\n customHookDepth: { warn: 3, error: 5 },\n};\n","import { scoreEffect } from \"../scoring/effect-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isHookCall } from \"../ast-helpers.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport interface RuleContext {\n options: unknown[];\n filename?: string;\n cwd?: string;\n report: (d: {\n message: string;\n node: unknown;\n severity?: \"warn\" | \"error\";\n }) => void;\n}\n\nexport const noFatEffects = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag dense useEffect blocks\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.fatEffect.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.fatEffect.error;\n return {\n CallExpression(node: any) {\n if (!isHookCall(node, \"useEffect\")) return;\n const score = scoreEffect(node);\n if (score.total >= threshold) {\n const breakdown = `deps=${score.deps} branches=${score.branches} ` +\n `setStates=${score.setStateCount} nested=${score.nestedEffects}` +\n (score.hasSubscriptionLike && !score.hasCleanup\n ? \" missing-cleanup\"\n : \"\");\n const severity = score.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message: `useEffect entropy ${\n score.total.toFixed(1)\n } ≥ ${threshold} (${breakdown})`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface StateScore {\n useStateCount: number;\n correlatedSetters: number;\n total: number;\n}\n\nexport function scoreComponentState(componentNode: Node): StateScore {\n const setterNames = new Set<string>();\n let useStateCount = 0;\n\n // First pass: collect useState setters\n walk(componentNode, (n) => {\n if (!n) return true;\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n useStateCount++;\n const els = (id as any).elements as Node[];\n const setter = els?.[1];\n if (setter?.type === \"Identifier\") {\n setterNames.add((setter as any).name as string);\n }\n }\n }\n return true;\n });\n\n // Second pass: count correlated setters\n let correlatedSetters = 0;\n walk(componentNode, (n) => {\n if (!n) return true;\n if (\n n.type === \"FunctionDeclaration\" ||\n n.type === \"FunctionExpression\" ||\n n.type === \"ArrowFunctionExpression\"\n ) {\n const calledSetters = new Set<string>();\n walk(n, (m) => {\n if (!m) return true;\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const name = (callee as any).name as string;\n if (setterNames.has(name)) calledSetters.add(name);\n }\n }\n return true;\n });\n if (calledSetters.size >= 2) correlatedSetters += calledSetters.size;\n }\n return true;\n });\n\n const total = useStateCount + correlatedSetters * 0.5;\n return { useStateCount, correlatedSetters, total };\n}\n","import { scoreComponentState } from \"../scoring/state-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport const stateScatter = {\n meta: {\n type: \"suggestion\" as const,\n docs: { description: \"Flag components with too many useState calls\" },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.stateScatter.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.stateScatter.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreComponentState(node);\n if (s.total >= threshold) {\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n context.report({\n message:\n `state scatter ${s.total} ≥ ${threshold} (useStates=${s.useStateCount}, correlated setters=${s.correlatedSetters}). Consider useReducer.`,\n node,\n severity,\n });\n }\n }\n return {\n FunctionDeclaration: check,\n VariableDeclaration: check,\n };\n },\n};\n","import { isHookCall, walk } from \"../ast-helpers.ts\";\n\ntype Node = { type: string; [k: string]: unknown };\n\nexport interface CouplingScore {\n total: number;\n readWriteSame: Array<{ state: string; effect: Node }>;\n}\n\nexport function scoreCoupling(componentNode: Node): CouplingScore {\n const stateBySetter = new Map<string, string>();\n walk(componentNode, (n) => {\n if (n.type === \"VariableDeclarator\") {\n const init = (n as any).init as Node | undefined;\n const id = (n as any).id as Node | undefined;\n if (\n init?.type === \"CallExpression\" &&\n isHookCall(init, \"useState\") &&\n id?.type === \"ArrayPattern\"\n ) {\n const els = (id as any).elements as Node[];\n const stateId = els?.[0];\n const setterId = els?.[1];\n if (stateId?.type === \"Identifier\" && setterId?.type === \"Identifier\") {\n stateBySetter.set(\n (setterId as any).name as string,\n (stateId as any).name as string,\n );\n }\n }\n }\n return true;\n });\n\n const readWriteSame: Array<{ state: string; effect: Node }> = [];\n let total = 0;\n\n walk(componentNode, (n) => {\n if (n.type === \"CallExpression\" && isHookCall(n, \"useEffect\")) {\n const effectFn = ((n as any).arguments as Node[])?.[0];\n if (!effectFn) return true;\n\n const stateRefs = new Set<string>();\n const stateWrites = new Set<string>();\n const stateNames = new Set(stateBySetter.values());\n\n walk(effectFn, (m) => {\n if (m.type === \"Identifier\") {\n const name = (m as any).name as string;\n if (stateNames.has(name)) stateRefs.add(name);\n }\n if (m.type === \"CallExpression\") {\n const callee = (m as any).callee as Node;\n if (callee?.type === \"Identifier\") {\n const setter = (callee as any).name as string;\n const stateName = stateBySetter.get(setter);\n if (stateName) stateWrites.add(stateName);\n }\n }\n return true;\n });\n\n for (const written of stateWrites) {\n if (stateRefs.has(written)) {\n readWriteSame.push({ state: written, effect: n });\n total += 3;\n }\n }\n }\n return true;\n });\n\n return { total, readWriteSame };\n}\n","import { scoreCoupling } from \"../scoring/coupling-score.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { isReactComponent } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n threshold?: number;\n errorThreshold?: number;\n}\n\nexport const hookCoupling = {\n meta: {\n type: \"problem\" as const,\n docs: {\n description: \"Flag effects that read state they also write (loop bait)\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const threshold = opts.threshold ?? DEFAULT_THRESHOLDS.hookCoupling.warn;\n const errorThreshold = opts.errorThreshold ??\n DEFAULT_THRESHOLDS.hookCoupling.error;\n function check(node: any) {\n if (!isReactComponent(node)) return;\n const s = scoreCoupling(node);\n if (s.total < threshold) return;\n const severity = s.total >= errorThreshold ? \"error\" : \"warn\";\n for (const v of s.readWriteSame) {\n context.report({\n message:\n `useEffect reads + writes same state '${v.state}' (loop risk)`,\n node: v.effect,\n severity,\n });\n }\n }\n return { FunctionDeclaration: check, VariableDeclaration: check };\n },\n};\n","import ts from \"typescript\";\n\nexport class TsProgramCache {\n private program: ts.Program | null = null;\n private rootDir: string;\n\n constructor(rootDir: string) {\n this.rootDir = rootDir;\n }\n\n private getProgram(): ts.Program {\n if (this.program) return this.program;\n const configPath = ts.findConfigFile(\n this.rootDir,\n ts.sys.fileExists,\n \"tsconfig.json\",\n );\n let compilerOptions: ts.CompilerOptions = {\n target: ts.ScriptTarget.ESNext,\n module: ts.ModuleKind.ESNext,\n jsx: ts.JsxEmit.Preserve,\n moduleResolution: ts.ModuleResolutionKind.Bundler,\n allowJs: true,\n noEmit: true,\n skipLibCheck: true,\n strict: false,\n };\n let fileNames: string[] = [];\n if (configPath) {\n const cfg = ts.readConfigFile(configPath, ts.sys.readFile);\n const configDir = configPath.slice(0, configPath.lastIndexOf(\"/\"));\n const parsed = ts.parseJsonConfigFileContent(\n cfg.config,\n ts.sys,\n configDir,\n );\n compilerOptions = { ...compilerOptions, ...parsed.options };\n fileNames = parsed.fileNames;\n }\n this.program = ts.createProgram(fileNames, compilerOptions);\n return this.program;\n }\n\n resolveIdentifierDeclaration(\n filePath: string,\n identifier: string,\n ): ts.Declaration | null {\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n const absolute = filePath.startsWith(\"/\")\n ? filePath\n : `${this.rootDir.replace(/\\/$/, \"\")}/${filePath}`;\n const sourceFile = program.getSourceFile(absolute) ??\n program.getSourceFile(filePath);\n if (!sourceFile) return null;\n\n let target: ts.Node | null = null;\n function find(node: ts.Node) {\n if (target) return;\n if (ts.isIdentifier(node) && node.text === identifier) {\n target = node;\n return;\n }\n ts.forEachChild(node, find);\n }\n find(sourceFile);\n if (!target) return null;\n\n const symbol = checker.getSymbolAtLocation(target);\n if (!symbol) return null;\n const aliased = symbol.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(symbol)\n : symbol;\n return aliased.declarations?.[0] ?? null;\n }\n\n countTransitiveHookCalls(\n decl: ts.Declaration,\n depth = 0,\n seen = new Set<ts.Declaration>(),\n ): number {\n if (depth > 10 || seen.has(decl)) return depth;\n seen.add(decl);\n const program = this.getProgram();\n const checker = program.getTypeChecker();\n let maxDepth = depth;\n\n const visit = (node: ts.Node) => {\n if (\n ts.isCallExpression(node) &&\n ts.isIdentifier(node.expression) &&\n /^use[A-Z]/.test(node.expression.text)\n ) {\n const sym = checker.getSymbolAtLocation(node.expression);\n if (sym) {\n const aliased = sym.flags & ts.SymbolFlags.Alias\n ? checker.getAliasedSymbol(sym)\n : sym;\n const innerDecl = aliased.declarations?.[0];\n if (innerDecl) {\n const sf = innerDecl.getSourceFile();\n if (\n sf.fileName.includes(\"node_modules/@types/react\") ||\n sf.fileName.includes(\"node_modules/react/\")\n ) {\n maxDepth = Math.max(maxDepth, depth + 1);\n } else {\n const childDepth = this.countTransitiveHookCalls(\n innerDecl,\n depth + 1,\n seen,\n );\n maxDepth = Math.max(maxDepth, childDepth);\n }\n }\n }\n }\n ts.forEachChild(node, visit);\n };\n\n visit(decl);\n return maxDepth;\n }\n}\n","import { TsProgramCache } from \"../ts-program.ts\";\nimport { DEFAULT_THRESHOLDS } from \"../scoring/thresholds.ts\";\nimport { getHookName } from \"../ast-helpers.ts\";\nimport type { RuleContext } from \"./no-fat-effects.ts\";\n\ninterface Options {\n maxDepth?: number;\n errorMaxDepth?: number;\n}\n\nconst REACT_HOOKS = new Set([\n \"useState\",\n \"useEffect\",\n \"useLayoutEffect\",\n \"useMemo\",\n \"useCallback\",\n \"useReducer\",\n \"useContext\",\n \"useRef\",\n \"useImperativeHandle\",\n \"useDebugValue\",\n \"useId\",\n \"useTransition\",\n \"useDeferredValue\",\n \"useSyncExternalStore\",\n \"useInsertionEffect\",\n]);\n\nlet sharedCache: TsProgramCache | null = null;\n\nexport const customHookDepth = {\n meta: {\n type: \"suggestion\" as const,\n docs: {\n description:\n \"Flag custom hooks whose transitive nesting exceeds maxDepth (type-aware).\",\n },\n },\n create(context: RuleContext) {\n const opts = (context.options[0] as Options | undefined) ?? {};\n const maxDepth = opts.maxDepth ?? DEFAULT_THRESHOLDS.customHookDepth.warn;\n const errorMaxDepth = opts.errorMaxDepth ??\n DEFAULT_THRESHOLDS.customHookDepth.error;\n const g = globalThis as {\n Deno?: { cwd(): string };\n process?: { cwd(): string };\n };\n const cwd = context.cwd ?? g.process?.cwd() ?? g.Deno?.cwd() ?? \".\";\n sharedCache ??= new TsProgramCache(cwd);\n const cache = sharedCache;\n const filename = context.filename;\n\n return {\n CallExpression(node: any) {\n const name = getHookName(node);\n if (!name || REACT_HOOKS.has(name)) return;\n if (!filename) return;\n const decl = cache.resolveIdentifierDeclaration(filename, name);\n if (!decl) return;\n const depth = cache.countTransitiveHookCalls(decl);\n if (depth >= maxDepth) {\n const severity = depth >= errorMaxDepth ? \"error\" : \"warn\";\n context.report({\n message:\n `custom hook '${name}' transitive depth ${depth} ≥ ${maxDepth}`,\n node,\n severity,\n });\n }\n },\n };\n },\n};\n","import type { Rule } from \"@oxlint/plugins\";\nimport { noFatEffects } from \"./no-fat-effects.ts\";\nimport { stateScatter } from \"./state-scatter.ts\";\nimport { hookCoupling } from \"./hook-coupling.ts\";\nimport { customHookDepth } from \"./custom-hook-depth.ts\";\n\n// Rules use our internal ESLint-compatible RuleContext shape; eslintCompatPlugin\n// adapts them to oxlint's stricter Rule contract at runtime.\nexport const ALL_RULES: Record<string, Rule> = {\n \"no-fat-effects\": noFatEffects as unknown as Rule,\n \"state-scatter\": stateScatter as unknown as Rule,\n \"hook-coupling\": hookCoupling as unknown as Rule,\n \"custom-hook-depth\": customHookDepth as unknown as Rule,\n};\n\nexport type RuleId =\n | \"no-fat-effects\"\n | \"state-scatter\"\n | \"hook-coupling\"\n | \"custom-hook-depth\";\n"],"mappings":"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"}
|