aislop 0.1.3 → 0.2.1
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 +136 -260
- package/dist/cli.js +254 -126
- package/dist/{engine-info-Bi8pE12U.js → engine-info-DFze-2GQ.js} +1 -1
- package/dist/index.js +464 -362
- package/dist/{json-66-1kHeg.js → json-Ci_gvHLS.js} +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -872,6 +872,7 @@ const JS_EXTENSIONS = new Set([
|
|
|
872
872
|
".cjs"
|
|
873
873
|
]);
|
|
874
874
|
const PY_EXTENSIONS = new Set([".py"]);
|
|
875
|
+
const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
|
|
875
876
|
const extractJsImportedSymbols = (lines) => {
|
|
876
877
|
const symbols = [];
|
|
877
878
|
const importLines = /* @__PURE__ */ new Set();
|
|
@@ -980,32 +981,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
|
|
|
980
981
|
}
|
|
981
982
|
return false;
|
|
982
983
|
};
|
|
984
|
+
const analyzeFile = (filePath) => {
|
|
985
|
+
if (isAutoGenerated(filePath)) return null;
|
|
986
|
+
let content;
|
|
987
|
+
try {
|
|
988
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
989
|
+
} catch {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
const ext = path.extname(filePath);
|
|
993
|
+
const lines = content.split("\n");
|
|
994
|
+
let symbols;
|
|
995
|
+
let importLines;
|
|
996
|
+
if (JS_EXTENSIONS.has(ext)) {
|
|
997
|
+
const result = extractJsImportedSymbols(lines);
|
|
998
|
+
symbols = result.symbols;
|
|
999
|
+
importLines = result.importLines;
|
|
1000
|
+
} else if (PY_EXTENSIONS.has(ext)) {
|
|
1001
|
+
const result = extractPyImportedSymbols(lines);
|
|
1002
|
+
symbols = result.symbols;
|
|
1003
|
+
importLines = result.importLines;
|
|
1004
|
+
} else return null;
|
|
1005
|
+
return {
|
|
1006
|
+
lines,
|
|
1007
|
+
symbols,
|
|
1008
|
+
importLines,
|
|
1009
|
+
ext
|
|
1010
|
+
};
|
|
1011
|
+
};
|
|
1012
|
+
const getUnusedSymbols = (lines, symbols, importLines) => {
|
|
1013
|
+
const content = lines.join("\n");
|
|
1014
|
+
return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
|
|
1015
|
+
};
|
|
983
1016
|
const detectUnusedImports = async (context) => {
|
|
984
1017
|
const files = getSourceFiles(context);
|
|
985
1018
|
const diagnostics = [];
|
|
986
1019
|
for (const filePath of files) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
try {
|
|
990
|
-
content = fs.readFileSync(filePath, "utf-8");
|
|
991
|
-
} catch {
|
|
992
|
-
continue;
|
|
993
|
-
}
|
|
994
|
-
const ext = path.extname(filePath);
|
|
1020
|
+
const analysis = analyzeFile(filePath);
|
|
1021
|
+
if (!analysis) continue;
|
|
995
1022
|
const relativePath = path.relative(context.rootDirectory, filePath);
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
let importLinesSet;
|
|
999
|
-
if (JS_EXTENSIONS.has(ext)) {
|
|
1000
|
-
const result = extractJsImportedSymbols(lines);
|
|
1001
|
-
symbols = result.symbols;
|
|
1002
|
-
importLinesSet = result.importLines;
|
|
1003
|
-
} else if (PY_EXTENSIONS.has(ext)) {
|
|
1004
|
-
const result = extractPyImportedSymbols(lines);
|
|
1005
|
-
symbols = result.symbols;
|
|
1006
|
-
importLinesSet = result.importLines;
|
|
1007
|
-
} else continue;
|
|
1008
|
-
for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
|
|
1023
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
1024
|
+
for (const symbol of unused) diagnostics.push({
|
|
1009
1025
|
filePath: relativePath,
|
|
1010
1026
|
engine: "ai-slop",
|
|
1011
1027
|
rule: "ai-slop/unused-import",
|
|
@@ -1643,13 +1659,41 @@ const isToolInstalled = async (tool) => {
|
|
|
1643
1659
|
//#region src/engines/code-quality/knip.ts
|
|
1644
1660
|
const KNIP_MESSAGE_MAP = {
|
|
1645
1661
|
files: "Unused file",
|
|
1662
|
+
dependencies: "Unused dependency",
|
|
1663
|
+
devDependencies: "Unused devDependency",
|
|
1664
|
+
unlisted: "Unlisted dependency",
|
|
1665
|
+
unresolved: "Unresolved import",
|
|
1666
|
+
binaries: "Unlisted binary",
|
|
1646
1667
|
exports: "Unused export",
|
|
1647
1668
|
types: "Unused type",
|
|
1648
1669
|
duplicates: "Duplicate export"
|
|
1649
1670
|
};
|
|
1671
|
+
const DEPENDENCY_TYPES = [
|
|
1672
|
+
"dependencies",
|
|
1673
|
+
"devDependencies",
|
|
1674
|
+
"unlisted",
|
|
1675
|
+
"unresolved",
|
|
1676
|
+
"binaries"
|
|
1677
|
+
];
|
|
1678
|
+
const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
|
|
1679
|
+
const getIssueItems = (fileIssue, issueType) => {
|
|
1680
|
+
const items = fileIssue[issueType];
|
|
1681
|
+
return Array.isArray(items) ? items : [];
|
|
1682
|
+
};
|
|
1683
|
+
const DEPENDENCY_HELP = {
|
|
1684
|
+
dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1685
|
+
devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
|
|
1686
|
+
unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
|
|
1687
|
+
unresolved: "This import cannot be resolved. Check for typos or missing packages.",
|
|
1688
|
+
binaries: "This binary is used but its package is not in package.json."
|
|
1689
|
+
};
|
|
1650
1690
|
const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
1651
1691
|
const diagnostics = [];
|
|
1652
|
-
const issues =
|
|
1692
|
+
const issues = getIssueItems(fileIssue, issueType);
|
|
1693
|
+
const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
|
|
1694
|
+
const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
|
|
1695
|
+
const fixable = issueType === "dependencies" || issueType === "devDependencies";
|
|
1696
|
+
const help = DEPENDENCY_HELP[issueType] ?? "";
|
|
1653
1697
|
for (const issue of issues) {
|
|
1654
1698
|
const symbol = issue.name ?? issue.symbol ?? "unknown";
|
|
1655
1699
|
const absolutePath = path.resolve(knipCwd, fileIssue.file);
|
|
@@ -1657,13 +1701,13 @@ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
|
|
|
1657
1701
|
filePath: path.relative(rootDir, absolutePath),
|
|
1658
1702
|
engine: "code-quality",
|
|
1659
1703
|
rule: `knip/${issueType}`,
|
|
1660
|
-
severity
|
|
1704
|
+
severity,
|
|
1661
1705
|
message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
|
|
1662
|
-
help
|
|
1706
|
+
help,
|
|
1663
1707
|
line: issue.line ?? 0,
|
|
1664
1708
|
column: issue.col ?? 0,
|
|
1665
|
-
category
|
|
1666
|
-
fixable
|
|
1709
|
+
category,
|
|
1710
|
+
fixable
|
|
1667
1711
|
});
|
|
1668
1712
|
}
|
|
1669
1713
|
return diagnostics;
|
|
@@ -1697,6 +1741,38 @@ const findKnipBin = (rootDirectory, monorepoRoot) => {
|
|
|
1697
1741
|
}
|
|
1698
1742
|
return null;
|
|
1699
1743
|
};
|
|
1744
|
+
const runKnipDependencyCheck = async (rootDirectory) => {
|
|
1745
|
+
return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
|
|
1746
|
+
};
|
|
1747
|
+
const fixUnusedDependencies = async (rootDirectory) => {
|
|
1748
|
+
const diagnostics = await runKnipDependencyCheck(rootDirectory);
|
|
1749
|
+
if (diagnostics.length === 0) return;
|
|
1750
|
+
const pkgPath = path.join(rootDirectory, "package.json");
|
|
1751
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
1752
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
1753
|
+
const pkg = JSON.parse(raw);
|
|
1754
|
+
const unusedDeps = /* @__PURE__ */ new Set();
|
|
1755
|
+
const unusedDevDeps = /* @__PURE__ */ new Set();
|
|
1756
|
+
for (const d of diagnostics) {
|
|
1757
|
+
const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
|
|
1758
|
+
if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
|
|
1759
|
+
if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
|
|
1760
|
+
}
|
|
1761
|
+
let changed = false;
|
|
1762
|
+
if (pkg.dependencies) {
|
|
1763
|
+
for (const name of unusedDeps) if (name in pkg.dependencies) {
|
|
1764
|
+
delete pkg.dependencies[name];
|
|
1765
|
+
changed = true;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
if (pkg.devDependencies) {
|
|
1769
|
+
for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
|
|
1770
|
+
delete pkg.devDependencies[name];
|
|
1771
|
+
changed = true;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
|
|
1775
|
+
};
|
|
1700
1776
|
const runKnip = async (rootDirectory) => {
|
|
1701
1777
|
const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
|
|
1702
1778
|
if (!knipRuntime) return [];
|
|
@@ -1730,11 +1806,13 @@ const runKnip = async (rootDirectory) => {
|
|
|
1730
1806
|
fixable: false
|
|
1731
1807
|
});
|
|
1732
1808
|
const issues = parsed.issues ?? [];
|
|
1733
|
-
|
|
1809
|
+
const issueTypes = [
|
|
1810
|
+
...DEPENDENCY_TYPES,
|
|
1734
1811
|
"exports",
|
|
1735
1812
|
"types",
|
|
1736
1813
|
"duplicates"
|
|
1737
|
-
]
|
|
1814
|
+
];
|
|
1815
|
+
for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
|
|
1738
1816
|
return diagnostics;
|
|
1739
1817
|
} catch {
|
|
1740
1818
|
return [];
|
|
@@ -1860,14 +1938,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
|
|
|
1860
1938
|
const fixBiomeFormat = async (context) => {
|
|
1861
1939
|
const targets = getBiomeTargets(context);
|
|
1862
1940
|
if (targets.length === 0) return;
|
|
1863
|
-
|
|
1864
|
-
"
|
|
1941
|
+
await runBiome([
|
|
1942
|
+
"format",
|
|
1865
1943
|
"--write",
|
|
1866
|
-
"--formatter-enabled=true",
|
|
1867
|
-
"--linter-enabled=false",
|
|
1868
1944
|
...targets
|
|
1869
1945
|
], context.rootDirectory, 6e4);
|
|
1870
|
-
if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
|
|
1871
1946
|
};
|
|
1872
1947
|
|
|
1873
1948
|
//#endregion
|
|
@@ -2430,6 +2505,7 @@ const fixOxlint = async (context) => {
|
|
|
2430
2505
|
"-c",
|
|
2431
2506
|
configPath,
|
|
2432
2507
|
"--fix",
|
|
2508
|
+
"--fix-suggestions",
|
|
2433
2509
|
"."
|
|
2434
2510
|
];
|
|
2435
2511
|
const result = await runSubprocess(process.execPath, args, {
|
|
@@ -3118,14 +3194,13 @@ const logger = {
|
|
|
3118
3194
|
* Application version — injected at build time by tsdown from package.json.
|
|
3119
3195
|
* The fallback should always match the "version" field in package.json.
|
|
3120
3196
|
*/
|
|
3121
|
-
const APP_VERSION = "0.1
|
|
3197
|
+
const APP_VERSION = "0.2.1";
|
|
3122
3198
|
|
|
3123
3199
|
//#endregion
|
|
3124
3200
|
//#region src/output/layout.ts
|
|
3125
3201
|
const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
|
|
3126
3202
|
const printCommandHeader = (commandName) => {
|
|
3127
|
-
logger.log(highlighter.bold(`aislop ${commandName}`));
|
|
3128
|
-
logger.log(highlighter.dim(`v${APP_VERSION}`));
|
|
3203
|
+
logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
|
|
3129
3204
|
logger.break();
|
|
3130
3205
|
};
|
|
3131
3206
|
const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
|
|
@@ -3136,81 +3211,6 @@ const printProjectMetadata = (project) => {
|
|
|
3136
3211
|
logger.break();
|
|
3137
3212
|
};
|
|
3138
3213
|
|
|
3139
|
-
//#endregion
|
|
3140
|
-
//#region src/output/pager.ts
|
|
3141
|
-
const DEFAULT_COLUMNS = 80;
|
|
3142
|
-
const DEFAULT_ROWS = 24;
|
|
3143
|
-
const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
|
|
3144
|
-
const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
|
|
3145
|
-
const resolvePagerCommand = () => {
|
|
3146
|
-
const pager = process.env.PAGER?.trim();
|
|
3147
|
-
if (pager) {
|
|
3148
|
-
const [command, ...args] = pager.split(/\s+/);
|
|
3149
|
-
if (command) return {
|
|
3150
|
-
command,
|
|
3151
|
-
args
|
|
3152
|
-
};
|
|
3153
|
-
}
|
|
3154
|
-
return {
|
|
3155
|
-
command: "less",
|
|
3156
|
-
args: [
|
|
3157
|
-
"-R",
|
|
3158
|
-
"-F",
|
|
3159
|
-
"-X"
|
|
3160
|
-
]
|
|
3161
|
-
};
|
|
3162
|
-
};
|
|
3163
|
-
const writeToStdout = (text) => {
|
|
3164
|
-
process.stdout.write(text);
|
|
3165
|
-
};
|
|
3166
|
-
const pipeToPager = async (command, args, text) => new Promise((resolve) => {
|
|
3167
|
-
let settled = false;
|
|
3168
|
-
const finish = (success) => {
|
|
3169
|
-
if (settled) return;
|
|
3170
|
-
settled = true;
|
|
3171
|
-
resolve(success);
|
|
3172
|
-
};
|
|
3173
|
-
try {
|
|
3174
|
-
const child = spawn(command, args, {
|
|
3175
|
-
stdio: [
|
|
3176
|
-
"pipe",
|
|
3177
|
-
"inherit",
|
|
3178
|
-
"inherit"
|
|
3179
|
-
],
|
|
3180
|
-
windowsHide: true
|
|
3181
|
-
});
|
|
3182
|
-
child.once("error", () => finish(false));
|
|
3183
|
-
child.once("close", (code) => finish(code === 0));
|
|
3184
|
-
child.stdin?.on("error", () => void 0);
|
|
3185
|
-
child.stdin?.end(text);
|
|
3186
|
-
} catch {
|
|
3187
|
-
finish(false);
|
|
3188
|
-
}
|
|
3189
|
-
});
|
|
3190
|
-
const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
|
|
3191
|
-
const width = Math.max(1, columns);
|
|
3192
|
-
return text.split("\n").reduce((count, line) => {
|
|
3193
|
-
const visibleLine = stripAnsi(line).replaceAll(" ", " ");
|
|
3194
|
-
return count + Math.max(1, Math.ceil(visibleLine.length / width));
|
|
3195
|
-
}, 0);
|
|
3196
|
-
};
|
|
3197
|
-
const shouldPageOutput = (text, options = {}) => {
|
|
3198
|
-
if (text.trim().length === 0) return false;
|
|
3199
|
-
const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
3200
|
-
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
3201
|
-
if (!stdinIsTTY || !stdoutIsTTY) return false;
|
|
3202
|
-
const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
|
|
3203
|
-
return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
|
|
3204
|
-
};
|
|
3205
|
-
const printMaybePaged = async (text) => {
|
|
3206
|
-
if (!shouldPageOutput(text)) {
|
|
3207
|
-
writeToStdout(text);
|
|
3208
|
-
return;
|
|
3209
|
-
}
|
|
3210
|
-
const pager = resolvePagerCommand();
|
|
3211
|
-
if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
|
|
3212
|
-
};
|
|
3213
|
-
|
|
3214
3214
|
//#endregion
|
|
3215
3215
|
//#region src/output/scan-progress.ts
|
|
3216
3216
|
const SPINNER_FRAMES = [
|
|
@@ -3692,9 +3692,13 @@ const getAnonymousId = () => {
|
|
|
3692
3692
|
for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
|
|
3693
3693
|
return `aislop_${(hash >>> 0).toString(36)}`;
|
|
3694
3694
|
};
|
|
3695
|
+
/** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
|
|
3696
|
+
let pendingRequest = null;
|
|
3695
3697
|
/**
|
|
3696
3698
|
* Fire-and-forget telemetry event to PostHog.
|
|
3697
|
-
* Never throws, never blocks
|
|
3699
|
+
* Never throws, never blocks CLI output.
|
|
3700
|
+
* The request is kept alive via `flushTelemetry()` so Node doesn't
|
|
3701
|
+
* exit before it completes.
|
|
3698
3702
|
*/
|
|
3699
3703
|
const trackEvent = (event) => {
|
|
3700
3704
|
const payload = {
|
|
@@ -3717,12 +3721,23 @@ const trackEvent = (event) => {
|
|
|
3717
3721
|
},
|
|
3718
3722
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3719
3723
|
};
|
|
3720
|
-
fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3724
|
+
pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
|
|
3721
3725
|
method: "POST",
|
|
3722
3726
|
headers: { "Content-Type": "application/json" },
|
|
3723
3727
|
body: JSON.stringify(payload),
|
|
3724
3728
|
signal: AbortSignal.timeout(3e3)
|
|
3725
|
-
}).catch(() => {});
|
|
3729
|
+
}).then(() => {}).catch(() => {});
|
|
3730
|
+
};
|
|
3731
|
+
/**
|
|
3732
|
+
* Wait for any pending telemetry request to complete.
|
|
3733
|
+
* Call this before `process.exit()` to ensure the event is delivered.
|
|
3734
|
+
* Times out after 3 seconds so it never hangs the CLI.
|
|
3735
|
+
*/
|
|
3736
|
+
const flushTelemetry = async () => {
|
|
3737
|
+
if (pendingRequest) {
|
|
3738
|
+
await pendingRequest;
|
|
3739
|
+
pendingRequest = null;
|
|
3740
|
+
}
|
|
3726
3741
|
};
|
|
3727
3742
|
|
|
3728
3743
|
//#endregion
|
|
@@ -3805,12 +3820,13 @@ const scanCommand = async (directory, config, options) => {
|
|
|
3805
3820
|
console.log(JSON.stringify(jsonOut, null, 2));
|
|
3806
3821
|
return { exitCode };
|
|
3807
3822
|
}
|
|
3808
|
-
|
|
3823
|
+
const output = [
|
|
3809
3824
|
"",
|
|
3810
3825
|
allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
|
|
3811
3826
|
renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
|
|
3812
3827
|
""
|
|
3813
|
-
].join("\n")
|
|
3828
|
+
].join("\n");
|
|
3829
|
+
process.stdout.write(output);
|
|
3814
3830
|
return { exitCode };
|
|
3815
3831
|
};
|
|
3816
3832
|
|
|
@@ -3960,6 +3976,99 @@ const doctorCommand = async (directory) => {
|
|
|
3960
3976
|
printDoctorConclusion(isAllGood());
|
|
3961
3977
|
};
|
|
3962
3978
|
|
|
3979
|
+
//#endregion
|
|
3980
|
+
//#region src/engines/ai-slop/unused-imports-fix.ts
|
|
3981
|
+
const fixUnusedImports = async (context) => {
|
|
3982
|
+
const files = getSourceFiles(context);
|
|
3983
|
+
for (const filePath of files) {
|
|
3984
|
+
const analysis = analyzeFile(filePath);
|
|
3985
|
+
if (!analysis) continue;
|
|
3986
|
+
const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
|
|
3987
|
+
if (unused.length === 0) continue;
|
|
3988
|
+
const unusedNames = new Set(unused.map((u) => u.name));
|
|
3989
|
+
const lines = [...analysis.lines];
|
|
3990
|
+
const symbolsByLine = /* @__PURE__ */ new Map();
|
|
3991
|
+
for (const sym of analysis.symbols) {
|
|
3992
|
+
const arr = symbolsByLine.get(sym.line) ?? [];
|
|
3993
|
+
arr.push(sym);
|
|
3994
|
+
symbolsByLine.set(sym.line, arr);
|
|
3995
|
+
}
|
|
3996
|
+
const linesToRemove = /* @__PURE__ */ new Set();
|
|
3997
|
+
for (const [lineNo, syms] of symbolsByLine) {
|
|
3998
|
+
const lineIdx = lineNo - 1;
|
|
3999
|
+
const allUnused = syms.every((s) => unusedNames.has(s.name));
|
|
4000
|
+
const importSpan = getImportSpan(lineIdx, analysis.importLines);
|
|
4001
|
+
if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
|
|
4002
|
+
else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
|
|
4003
|
+
else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
|
|
4004
|
+
}
|
|
4005
|
+
if (linesToRemove.size === 0 && unused.length === 0) continue;
|
|
4006
|
+
const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
|
|
4007
|
+
for (const idx of sortedRemove) lines.splice(idx, 1);
|
|
4008
|
+
const filtered = lines.filter((l) => l !== REMOVE_MARKER);
|
|
4009
|
+
while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
|
|
4010
|
+
fs.writeFileSync(filePath, filtered.join("\n"));
|
|
4011
|
+
}
|
|
4012
|
+
};
|
|
4013
|
+
const getImportSpan = (startIdx, importLines) => {
|
|
4014
|
+
const span = [startIdx];
|
|
4015
|
+
let idx = startIdx + 1;
|
|
4016
|
+
while (importLines.has(idx)) {
|
|
4017
|
+
span.push(idx);
|
|
4018
|
+
idx++;
|
|
4019
|
+
}
|
|
4020
|
+
return span;
|
|
4021
|
+
};
|
|
4022
|
+
const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
|
|
4023
|
+
const fullImport = span.map((i) => lines[i]).join("\n");
|
|
4024
|
+
const namedMatch = fullImport.match(/\{([^}]+)\}/s);
|
|
4025
|
+
if (!namedMatch) return;
|
|
4026
|
+
const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
|
|
4027
|
+
if (unusedNamed.length === 0) return;
|
|
4028
|
+
const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
|
|
4029
|
+
const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
|
|
4030
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4031
|
+
const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
|
|
4032
|
+
return !unusedNamedSet.has(localName);
|
|
4033
|
+
});
|
|
4034
|
+
if (keptSpecifiers.length === 0) {
|
|
4035
|
+
if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
|
|
4036
|
+
const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
|
|
4037
|
+
lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
|
|
4038
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4039
|
+
}
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
|
|
4043
|
+
const fromClause = fromMatch ? fromMatch[1].trim() : "";
|
|
4044
|
+
const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
|
|
4045
|
+
const prefix = importPrefix ? importPrefix[1] : "import ";
|
|
4046
|
+
const wasMultiLine = span.length > 1;
|
|
4047
|
+
let newImport;
|
|
4048
|
+
if (wasMultiLine && keptSpecifiers.length > 2) {
|
|
4049
|
+
const indentMatch = lines[span[1]]?.match(/^(\s+)/);
|
|
4050
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
4051
|
+
newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
|
|
4052
|
+
} else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
|
|
4053
|
+
lines[span[0]] = newImport;
|
|
4054
|
+
for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
|
|
4055
|
+
};
|
|
4056
|
+
const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
|
|
4057
|
+
const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
|
|
4058
|
+
if (!fromMatch) return;
|
|
4059
|
+
const prefix = fromMatch[1];
|
|
4060
|
+
const importPart = fromMatch[2].replace(/#.*$/, "").trim();
|
|
4061
|
+
const hasParen = importPart.startsWith("(");
|
|
4062
|
+
const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
|
|
4063
|
+
const parts = spec.split(/\s+as\s+/);
|
|
4064
|
+
const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
|
|
4065
|
+
return !unusedNames.has(localName);
|
|
4066
|
+
});
|
|
4067
|
+
if (keptSpecifiers.length === 0) return;
|
|
4068
|
+
const joined = keptSpecifiers.join(", ");
|
|
4069
|
+
lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
|
|
4070
|
+
};
|
|
4071
|
+
|
|
3963
4072
|
//#endregion
|
|
3964
4073
|
//#region src/commands/fix.ts
|
|
3965
4074
|
const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
|
|
@@ -4014,7 +4123,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
|
|
|
4014
4123
|
}
|
|
4015
4124
|
lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
|
|
4016
4125
|
if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
|
|
4017
|
-
|
|
4126
|
+
process.stdout.write(`${lines.join("\n")}\n\n`);
|
|
4018
4127
|
return result;
|
|
4019
4128
|
};
|
|
4020
4129
|
const createEngineContext = (rootDirectory, projectInfo, config) => ({
|
|
@@ -4057,6 +4166,15 @@ const fixCommand = async (directory, config, options = {
|
|
|
4057
4166
|
printProjectMetadata(projectInfo);
|
|
4058
4167
|
const context = createEngineContext(resolvedDir, projectInfo, config);
|
|
4059
4168
|
const steps = [];
|
|
4169
|
+
if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
|
|
4170
|
+
if (config.engines.lint) {
|
|
4171
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
4172
|
+
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
4173
|
+
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
4174
|
+
}
|
|
4175
|
+
if (config.engines["code-quality"]) {
|
|
4176
|
+
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
|
|
4177
|
+
}
|
|
4060
4178
|
if (config.engines.format) {
|
|
4061
4179
|
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
|
|
4062
4180
|
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
|
|
@@ -4064,11 +4182,6 @@ const fixCommand = async (directory, config, options = {
|
|
|
4064
4182
|
if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
|
|
4065
4183
|
else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
|
|
4066
4184
|
}
|
|
4067
|
-
if (config.engines.lint) {
|
|
4068
|
-
if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
|
|
4069
|
-
if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
|
|
4070
|
-
else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
|
|
4071
|
-
}
|
|
4072
4185
|
if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
|
|
4073
4186
|
else {
|
|
4074
4187
|
logger.break();
|
|
@@ -4148,6 +4261,11 @@ const BUILTIN_RULES = [
|
|
|
4148
4261
|
engine: "code-quality",
|
|
4149
4262
|
rules: [
|
|
4150
4263
|
"knip/files",
|
|
4264
|
+
"knip/dependencies",
|
|
4265
|
+
"knip/devDependencies",
|
|
4266
|
+
"knip/unlisted",
|
|
4267
|
+
"knip/unresolved",
|
|
4268
|
+
"knip/binaries",
|
|
4151
4269
|
"knip/exports",
|
|
4152
4270
|
"knip/types",
|
|
4153
4271
|
"complexity/file-too-large",
|
|
@@ -4204,7 +4322,7 @@ const rulesCommand = async (directory) => {
|
|
|
4204
4322
|
lines.push("");
|
|
4205
4323
|
}
|
|
4206
4324
|
}
|
|
4207
|
-
|
|
4325
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
4208
4326
|
};
|
|
4209
4327
|
|
|
4210
4328
|
//#endregion
|
|
@@ -4433,7 +4551,10 @@ const program = new Command().name("aislop").description("The unified code quali
|
|
|
4433
4551
|
verbose: Boolean(flags.verbose),
|
|
4434
4552
|
json: Boolean(flags.json)
|
|
4435
4553
|
});
|
|
4436
|
-
if (exitCode !== 0)
|
|
4554
|
+
if (exitCode !== 0) {
|
|
4555
|
+
await flushTelemetry();
|
|
4556
|
+
process.exit(exitCode);
|
|
4557
|
+
}
|
|
4437
4558
|
}).addHelpText("after", `
|
|
4438
4559
|
${highlighter.dim("Commands:")}
|
|
4439
4560
|
aislop scan [dir] Full code quality scan
|
|
@@ -4460,7 +4581,10 @@ program.command("scan [directory]").description("Run full code quality scan").op
|
|
|
4460
4581
|
verbose: Boolean(flags.verbose),
|
|
4461
4582
|
json: Boolean(flags.json)
|
|
4462
4583
|
});
|
|
4463
|
-
if (exitCode !== 0)
|
|
4584
|
+
if (exitCode !== 0) {
|
|
4585
|
+
await flushTelemetry();
|
|
4586
|
+
process.exit(exitCode);
|
|
4587
|
+
}
|
|
4464
4588
|
});
|
|
4465
4589
|
program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
|
|
4466
4590
|
const flags = command.optsWithGlobals();
|
|
@@ -4474,13 +4598,17 @@ program.command("doctor [directory]").description("Check installed tools and env
|
|
|
4474
4598
|
});
|
|
4475
4599
|
program.command("ci [directory]").description("CI-friendly JSON output with exit codes").action(async (directory = ".") => {
|
|
4476
4600
|
const { exitCode } = await ciCommand(directory, loadConfig(directory));
|
|
4477
|
-
if (exitCode !== 0)
|
|
4601
|
+
if (exitCode !== 0) {
|
|
4602
|
+
await flushTelemetry();
|
|
4603
|
+
process.exit(exitCode);
|
|
4604
|
+
}
|
|
4478
4605
|
});
|
|
4479
4606
|
program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
|
|
4480
4607
|
await rulesCommand(directory);
|
|
4481
4608
|
});
|
|
4482
4609
|
const main = async () => {
|
|
4483
4610
|
await program.parseAsync();
|
|
4611
|
+
await flushTelemetry();
|
|
4484
4612
|
};
|
|
4485
4613
|
main();
|
|
4486
4614
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Application version — injected at build time by tsdown from package.json.
|
|
4
4
|
* The fallback should always match the "version" field in package.json.
|
|
5
5
|
*/
|
|
6
|
-
const APP_VERSION = "0.1
|
|
6
|
+
const APP_VERSION = "0.2.1";
|
|
7
7
|
|
|
8
8
|
//#endregion
|
|
9
9
|
//#region src/output/engine-info.ts
|