slopbrick 0.15.0 → 0.17.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 +13 -14
- package/dist/engine/worker.cjs +1427 -22
- package/dist/engine/worker.js +1427 -22
- package/dist/index.cjs +19792 -18786
- package/dist/index.d.cts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +19789 -18783
- package/package.json +5 -3
package/dist/engine/worker.cjs
CHANGED
|
@@ -25,7 +25,7 @@ __export(worker_exports, {
|
|
|
25
25
|
});
|
|
26
26
|
module.exports = __toCommonJS(worker_exports);
|
|
27
27
|
var import_node_worker_threads = require("worker_threads");
|
|
28
|
-
var
|
|
28
|
+
var import_node_path11 = require("path");
|
|
29
29
|
|
|
30
30
|
// ../engine/dist/index.js
|
|
31
31
|
var import_promises = require("fs/promises");
|
|
@@ -33771,6 +33771,339 @@ var importPathMismatchRule = createRule({
|
|
|
33771
33771
|
}
|
|
33772
33772
|
});
|
|
33773
33773
|
|
|
33774
|
+
// src/rules/db/duplicate-index.ts
|
|
33775
|
+
var import_pgsql_parser = require("pgsql-parser");
|
|
33776
|
+
var moduleReady = (0, import_pgsql_parser.loadModule)();
|
|
33777
|
+
function parseStatements(raw) {
|
|
33778
|
+
let result;
|
|
33779
|
+
try {
|
|
33780
|
+
result = (0, import_pgsql_parser.parseSync)(raw);
|
|
33781
|
+
} catch {
|
|
33782
|
+
return null;
|
|
33783
|
+
}
|
|
33784
|
+
const stmts = [];
|
|
33785
|
+
for (const wrapper of result?.stmts ?? []) {
|
|
33786
|
+
const inner = wrapper.stmt ?? {};
|
|
33787
|
+
const type = Object.keys(inner)[0] ?? "Other";
|
|
33788
|
+
stmts.push({ type, ast: inner[type] });
|
|
33789
|
+
}
|
|
33790
|
+
return stmts;
|
|
33791
|
+
}
|
|
33792
|
+
var duplicateIndexRule = createRule({
|
|
33793
|
+
id: "db/duplicate-index",
|
|
33794
|
+
category: "db",
|
|
33795
|
+
severity: "high",
|
|
33796
|
+
aiSpecific: false,
|
|
33797
|
+
description: "Two CREATE INDEX statements cover the same column list on the same table \u2014 drop one.",
|
|
33798
|
+
create(context) {
|
|
33799
|
+
return context;
|
|
33800
|
+
},
|
|
33801
|
+
analyze(_context, facts) {
|
|
33802
|
+
const issues = [];
|
|
33803
|
+
const source = facts.v2?._source;
|
|
33804
|
+
if (!source) return issues;
|
|
33805
|
+
const stmts = parseStatements(source);
|
|
33806
|
+
if (!stmts) return issues;
|
|
33807
|
+
const seen = /* @__PURE__ */ new Map();
|
|
33808
|
+
for (const stmt of stmts) {
|
|
33809
|
+
if (stmt.type !== "IndexStmt") continue;
|
|
33810
|
+
const idx = stmt.ast;
|
|
33811
|
+
const idxName = idx.idxname ?? "?";
|
|
33812
|
+
const cols = (idx.indexParams ?? []).map((p) => p.IndexElem?.name ?? "?").sort();
|
|
33813
|
+
const key = cols.join(",");
|
|
33814
|
+
if (key === "") continue;
|
|
33815
|
+
const table = idx.relation?.relname ?? "";
|
|
33816
|
+
if (seen.has(key)) {
|
|
33817
|
+
const prev = seen.get(key);
|
|
33818
|
+
if (prev.indexName === idxName && prev.table === table) continue;
|
|
33819
|
+
issues.push({
|
|
33820
|
+
ruleId: "db/duplicate-index",
|
|
33821
|
+
category: "db",
|
|
33822
|
+
severity: "high",
|
|
33823
|
+
aiSpecific: false,
|
|
33824
|
+
message: `Index \`${idxName}\` duplicates \`${prev.indexName}\` on the same column list (${cols.join(", ")}).`,
|
|
33825
|
+
line: 1,
|
|
33826
|
+
column: 1,
|
|
33827
|
+
advice: `Drop one of the two indexes \u2014 extra indexes slow writes without benefit.`
|
|
33828
|
+
});
|
|
33829
|
+
} else {
|
|
33830
|
+
seen.set(key, { indexName: idxName, table });
|
|
33831
|
+
}
|
|
33832
|
+
}
|
|
33833
|
+
return issues;
|
|
33834
|
+
}
|
|
33835
|
+
});
|
|
33836
|
+
|
|
33837
|
+
// src/rules/db/enum-sprawl.ts
|
|
33838
|
+
var import_pgsql_parser2 = require("pgsql-parser");
|
|
33839
|
+
var moduleReady2 = (0, import_pgsql_parser2.loadModule)();
|
|
33840
|
+
function parseStatements2(raw) {
|
|
33841
|
+
let result;
|
|
33842
|
+
try {
|
|
33843
|
+
result = (0, import_pgsql_parser2.parseSync)(raw);
|
|
33844
|
+
} catch {
|
|
33845
|
+
return null;
|
|
33846
|
+
}
|
|
33847
|
+
const stmts = [];
|
|
33848
|
+
for (const wrapper of result?.stmts ?? []) {
|
|
33849
|
+
const inner = wrapper.stmt ?? {};
|
|
33850
|
+
const type = Object.keys(inner)[0] ?? "Other";
|
|
33851
|
+
stmts.push({ type, ast: inner[type] });
|
|
33852
|
+
}
|
|
33853
|
+
return stmts;
|
|
33854
|
+
}
|
|
33855
|
+
var ENUM_VALUES_MAX = 12;
|
|
33856
|
+
var enumSprawlRule = createRule({
|
|
33857
|
+
id: "db/enum-sprawl",
|
|
33858
|
+
category: "db",
|
|
33859
|
+
severity: "low",
|
|
33860
|
+
aiSpecific: false,
|
|
33861
|
+
description: `CREATE TYPE \u2026 AS ENUM with >${ENUM_VALUES_MAX} values \u2014 brittle to extend; prefer a lookup table.`,
|
|
33862
|
+
create(context) {
|
|
33863
|
+
return context;
|
|
33864
|
+
},
|
|
33865
|
+
analyze(_context, facts) {
|
|
33866
|
+
const issues = [];
|
|
33867
|
+
const source = facts.v2?._source;
|
|
33868
|
+
if (!source) return issues;
|
|
33869
|
+
const stmts = parseStatements2(source);
|
|
33870
|
+
if (!stmts) return issues;
|
|
33871
|
+
for (const stmt of stmts) {
|
|
33872
|
+
if (stmt.type !== "CreateEnumStmt") continue;
|
|
33873
|
+
const en = stmt.ast;
|
|
33874
|
+
const vals = (en.vals ?? []).map(
|
|
33875
|
+
(v) => v.String?.sval ?? "?"
|
|
33876
|
+
);
|
|
33877
|
+
if (vals.length <= ENUM_VALUES_MAX) continue;
|
|
33878
|
+
const typeName = (en.typeName?.names ?? []).map((n) => n.String?.sval).filter(Boolean).join(".");
|
|
33879
|
+
issues.push({
|
|
33880
|
+
ruleId: "db/enum-sprawl",
|
|
33881
|
+
category: "db",
|
|
33882
|
+
severity: "low",
|
|
33883
|
+
aiSpecific: false,
|
|
33884
|
+
message: `Enum \`${typeName}\` has ${vals.length} values (recommended max: ${ENUM_VALUES_MAX}).`,
|
|
33885
|
+
line: 1,
|
|
33886
|
+
column: 1,
|
|
33887
|
+
advice: `Consider a lookup table. Enums with many values are brittle to extend and hard to localize.`
|
|
33888
|
+
});
|
|
33889
|
+
}
|
|
33890
|
+
return issues;
|
|
33891
|
+
}
|
|
33892
|
+
});
|
|
33893
|
+
|
|
33894
|
+
// src/rules/db/missing-fk-index.ts
|
|
33895
|
+
var import_pgsql_parser3 = require("pgsql-parser");
|
|
33896
|
+
var moduleReady3 = (0, import_pgsql_parser3.loadModule)();
|
|
33897
|
+
function parseStatements3(raw) {
|
|
33898
|
+
let result;
|
|
33899
|
+
try {
|
|
33900
|
+
result = (0, import_pgsql_parser3.parseSync)(raw);
|
|
33901
|
+
} catch {
|
|
33902
|
+
return null;
|
|
33903
|
+
}
|
|
33904
|
+
const stmts = [];
|
|
33905
|
+
for (const wrapper of result?.stmts ?? []) {
|
|
33906
|
+
const inner = wrapper.stmt ?? {};
|
|
33907
|
+
const type = Object.keys(inner)[0] ?? "Other";
|
|
33908
|
+
stmts.push({ type, ast: inner[type] });
|
|
33909
|
+
}
|
|
33910
|
+
return stmts;
|
|
33911
|
+
}
|
|
33912
|
+
var missingFkIndexRule = createRule({
|
|
33913
|
+
id: "db/missing-fk-index",
|
|
33914
|
+
category: "db",
|
|
33915
|
+
severity: "high",
|
|
33916
|
+
aiSpecific: false,
|
|
33917
|
+
description: "Foreign key column without a matching CREATE INDEX in the same file \u2014 sequential scan on parent deletes.",
|
|
33918
|
+
create(context) {
|
|
33919
|
+
return context;
|
|
33920
|
+
},
|
|
33921
|
+
analyze(_context, facts) {
|
|
33922
|
+
const issues = [];
|
|
33923
|
+
const source = facts.v2?._source;
|
|
33924
|
+
if (!source) return issues;
|
|
33925
|
+
const stmts = parseStatements3(source);
|
|
33926
|
+
if (!stmts) return issues;
|
|
33927
|
+
const fkColsByTable = /* @__PURE__ */ new Map();
|
|
33928
|
+
const idxColsByTable = /* @__PURE__ */ new Map();
|
|
33929
|
+
for (const stmt of stmts) {
|
|
33930
|
+
if (stmt.type === "CreateStmt") {
|
|
33931
|
+
const cs = stmt.ast;
|
|
33932
|
+
const tableName = cs?.relation?.relname;
|
|
33933
|
+
if (!tableName || !Array.isArray(cs.tableElts)) continue;
|
|
33934
|
+
for (const elt of cs.tableElts) {
|
|
33935
|
+
const cd = elt.ColumnDef;
|
|
33936
|
+
if (!cd || !Array.isArray(cd.constraints)) continue;
|
|
33937
|
+
for (const con of cd.constraints) {
|
|
33938
|
+
const c = con.Constraint;
|
|
33939
|
+
if (!c) continue;
|
|
33940
|
+
const isFk = c.contype === "CONSTR_FOREIGN" || Array.isArray(c.fk_attrs) && c.fk_attrs.length > 0 || c.pktable?.relation?.relname;
|
|
33941
|
+
if (!isFk) continue;
|
|
33942
|
+
if (!fkColsByTable.has(tableName)) fkColsByTable.set(tableName, /* @__PURE__ */ new Set());
|
|
33943
|
+
fkColsByTable.get(tableName).add(cd.colname);
|
|
33944
|
+
}
|
|
33945
|
+
}
|
|
33946
|
+
} else if (stmt.type === "IndexStmt") {
|
|
33947
|
+
const idx = stmt.ast;
|
|
33948
|
+
const tableName = idx.relation?.relname;
|
|
33949
|
+
if (!tableName) continue;
|
|
33950
|
+
if (!idxColsByTable.has(tableName)) idxColsByTable.set(tableName, /* @__PURE__ */ new Set());
|
|
33951
|
+
for (const p of idx.indexParams ?? []) {
|
|
33952
|
+
if (p.IndexElem?.name) idxColsByTable.get(tableName).add(p.IndexElem.name);
|
|
33953
|
+
}
|
|
33954
|
+
}
|
|
33955
|
+
}
|
|
33956
|
+
for (const [table, fkCols] of fkColsByTable) {
|
|
33957
|
+
const idxCols = idxColsByTable.get(table) ?? /* @__PURE__ */ new Set();
|
|
33958
|
+
for (const fk of fkCols) {
|
|
33959
|
+
if (idxCols.has(fk)) continue;
|
|
33960
|
+
issues.push({
|
|
33961
|
+
ruleId: "db/missing-fk-index",
|
|
33962
|
+
category: "db",
|
|
33963
|
+
severity: "high",
|
|
33964
|
+
aiSpecific: false,
|
|
33965
|
+
message: `Foreign key column \`${table}.${fk}\` has no matching index.`,
|
|
33966
|
+
line: 1,
|
|
33967
|
+
column: 1,
|
|
33968
|
+
advice: `Add \`CREATE INDEX ON ${table} (${fk});\`. Without an index, deletes on the parent table perform a sequential scan. Use \`CREATE INDEX CONCURRENTLY\` in production.`,
|
|
33969
|
+
extras: { table, columnName: fk }
|
|
33970
|
+
});
|
|
33971
|
+
}
|
|
33972
|
+
}
|
|
33973
|
+
return issues;
|
|
33974
|
+
}
|
|
33975
|
+
});
|
|
33976
|
+
|
|
33977
|
+
// src/rules/db/missing-not-null.ts
|
|
33978
|
+
var import_pgsql_parser4 = require("pgsql-parser");
|
|
33979
|
+
var moduleReady4 = (0, import_pgsql_parser4.loadModule)();
|
|
33980
|
+
function parseStatements4(raw) {
|
|
33981
|
+
let result;
|
|
33982
|
+
try {
|
|
33983
|
+
result = (0, import_pgsql_parser4.parseSync)(raw);
|
|
33984
|
+
} catch {
|
|
33985
|
+
return null;
|
|
33986
|
+
}
|
|
33987
|
+
const stmts = [];
|
|
33988
|
+
for (const wrapper of result?.stmts ?? []) {
|
|
33989
|
+
const inner = wrapper.stmt ?? {};
|
|
33990
|
+
const type = Object.keys(inner)[0] ?? "Other";
|
|
33991
|
+
stmts.push({ type, ast: inner[type] });
|
|
33992
|
+
}
|
|
33993
|
+
return stmts;
|
|
33994
|
+
}
|
|
33995
|
+
var REQUIRED_COLUMN_HEURISTIC = /^(id|user_?id|order_?id|product_?id|tenant_?id|customer_?id|email|created_?at|updated_?at|deleted_?at|expires_?at|status|uuid|slug|handle|key|name)$/i;
|
|
33996
|
+
var missingNotNullRule = createRule({
|
|
33997
|
+
id: "db/missing-not-null",
|
|
33998
|
+
category: "db",
|
|
33999
|
+
severity: "high",
|
|
34000
|
+
aiSpecific: false,
|
|
34001
|
+
description: "Required-identifier column (id, email, created_at, \u2026) without NOT NULL \u2014 silent NULL inserts in production.",
|
|
34002
|
+
create(context) {
|
|
34003
|
+
return context;
|
|
34004
|
+
},
|
|
34005
|
+
analyze(_context, facts) {
|
|
34006
|
+
const issues = [];
|
|
34007
|
+
const source = facts.v2?._source;
|
|
34008
|
+
if (!source) return issues;
|
|
34009
|
+
const stmts = parseStatements4(source);
|
|
34010
|
+
if (!stmts) return issues;
|
|
34011
|
+
for (const stmt of stmts) {
|
|
34012
|
+
if (stmt.type !== "CreateStmt") continue;
|
|
34013
|
+
const cs = stmt.ast;
|
|
34014
|
+
const tableName = cs?.relation?.relname;
|
|
34015
|
+
if (!tableName || !Array.isArray(cs.tableElts)) continue;
|
|
34016
|
+
for (const elt of cs.tableElts) {
|
|
34017
|
+
const cd = elt.ColumnDef;
|
|
34018
|
+
if (!cd) continue;
|
|
34019
|
+
if (!REQUIRED_COLUMN_HEURISTIC.test(cd.colname ?? "")) continue;
|
|
34020
|
+
const hasNotNull = (cd.constraints ?? []).some((c) => {
|
|
34021
|
+
const con = c.Constraint;
|
|
34022
|
+
return con?.contype === "CONSTR_NOTNULL" || con?.contype === "CONSTR_PRIMARY";
|
|
34023
|
+
});
|
|
34024
|
+
if (hasNotNull) continue;
|
|
34025
|
+
issues.push({
|
|
34026
|
+
ruleId: "db/missing-not-null",
|
|
34027
|
+
category: "db",
|
|
34028
|
+
severity: "high",
|
|
34029
|
+
aiSpecific: false,
|
|
34030
|
+
message: `Required column \`${tableName}.${cd.colname}\` is missing \`NOT NULL\`.`,
|
|
34031
|
+
line: 1,
|
|
34032
|
+
column: 1,
|
|
34033
|
+
advice: `Add \`NOT NULL\` (or \`PRIMARY KEY\`). Optional identifiers are a common AI-generated SQL smell that leads to silent NULL inserts in production.`,
|
|
34034
|
+
extras: { table: tableName, columnName: cd.colname }
|
|
34035
|
+
});
|
|
34036
|
+
}
|
|
34037
|
+
}
|
|
34038
|
+
return issues;
|
|
34039
|
+
}
|
|
34040
|
+
});
|
|
34041
|
+
|
|
34042
|
+
// src/rules/db/naming-inconsistency.ts
|
|
34043
|
+
var import_pgsql_parser5 = require("pgsql-parser");
|
|
34044
|
+
var moduleReady5 = (0, import_pgsql_parser5.loadModule)();
|
|
34045
|
+
function parseStatements5(raw) {
|
|
34046
|
+
let result;
|
|
34047
|
+
try {
|
|
34048
|
+
result = (0, import_pgsql_parser5.parseSync)(raw);
|
|
34049
|
+
} catch {
|
|
34050
|
+
return null;
|
|
34051
|
+
}
|
|
34052
|
+
const stmts = [];
|
|
34053
|
+
for (const wrapper of result?.stmts ?? []) {
|
|
34054
|
+
const inner = wrapper.stmt ?? {};
|
|
34055
|
+
const type = Object.keys(inner)[0] ?? "Other";
|
|
34056
|
+
stmts.push({ type, ast: inner[type] });
|
|
34057
|
+
}
|
|
34058
|
+
return stmts;
|
|
34059
|
+
}
|
|
34060
|
+
var namingInconsistencyRule = createRule({
|
|
34061
|
+
id: "db/naming-inconsistency",
|
|
34062
|
+
category: "db",
|
|
34063
|
+
severity: "low",
|
|
34064
|
+
aiSpecific: false,
|
|
34065
|
+
description: "snake_case and camelCase identifiers mix within one SQL file \u2014 standardize.",
|
|
34066
|
+
create(context) {
|
|
34067
|
+
return context;
|
|
34068
|
+
},
|
|
34069
|
+
analyze(_context, facts) {
|
|
34070
|
+
const issues = [];
|
|
34071
|
+
const source = facts.v2?._source;
|
|
34072
|
+
if (!source) return issues;
|
|
34073
|
+
const stmts = parseStatements5(source);
|
|
34074
|
+
if (!stmts) return issues;
|
|
34075
|
+
let snakeCount = 0;
|
|
34076
|
+
let camelCount = 0;
|
|
34077
|
+
function walk3(node) {
|
|
34078
|
+
if (!node || typeof node !== "object") return;
|
|
34079
|
+
if (Array.isArray(node)) {
|
|
34080
|
+
for (const item of node) walk3(item);
|
|
34081
|
+
return;
|
|
34082
|
+
}
|
|
34083
|
+
if (node.String?.sval && typeof node.String.sval === "string") {
|
|
34084
|
+
const s = node.String.sval;
|
|
34085
|
+
if (/^[a-z_][a-z0-9_]*$/.test(s)) snakeCount++;
|
|
34086
|
+
else if (/^[a-z][A-Za-z0-9]*$/.test(s)) camelCount++;
|
|
34087
|
+
}
|
|
34088
|
+
for (const k of Object.keys(node)) walk3(node[k]);
|
|
34089
|
+
}
|
|
34090
|
+
walk3(stmts);
|
|
34091
|
+
if (snakeCount >= 2 && camelCount >= 2) {
|
|
34092
|
+
issues.push({
|
|
34093
|
+
ruleId: "db/naming-inconsistency",
|
|
34094
|
+
category: "db",
|
|
34095
|
+
severity: "low",
|
|
34096
|
+
aiSpecific: false,
|
|
34097
|
+
message: `Mixed identifier styles: ${snakeCount} snake_case vs ${camelCount} camelCase.`,
|
|
34098
|
+
line: 1,
|
|
34099
|
+
column: 1,
|
|
34100
|
+
advice: `Standardize on snake_case (Postgres convention) or document the deviation.`
|
|
34101
|
+
});
|
|
34102
|
+
}
|
|
34103
|
+
return issues;
|
|
34104
|
+
}
|
|
34105
|
+
});
|
|
34106
|
+
|
|
33774
34107
|
// src/rules/utils.ts
|
|
33775
34108
|
var LAYOUT_ARBITRARY_RE = /^(?:w|h|p|m|gap|space-x|space-y|px|py|mx|my|min-w|min-h|max-w|max-h|inset)-\[.*\]$/;
|
|
33776
34109
|
var RADIUS_ARBITRARY_RE = /^(?:rounded|rounded-t|rounded-r|rounded-b|rounded-l|rounded-tl|rounded-tr|rounded-br|rounded-bl|rounded-ss|rounded-se|rounded-es|rounded-ee)-\[.*\]$/;
|
|
@@ -33929,6 +34262,577 @@ function parseStyleObject(source) {
|
|
|
33929
34262
|
return entries;
|
|
33930
34263
|
}
|
|
33931
34264
|
|
|
34265
|
+
// src/rules/db/sql-concat.ts
|
|
34266
|
+
var TEMPLATE_SQL_RE = /`((?:SELECT|INSERT\s+INTO|UPDATE|DELETE\s+FROM|WITH)\b[^`]*\$\{[^}]+\}[^`]*)`/gi;
|
|
34267
|
+
var sqlConcatRule = createRule({
|
|
34268
|
+
id: "db/sql-concat",
|
|
34269
|
+
category: "db",
|
|
34270
|
+
severity: "high",
|
|
34271
|
+
aiSpecific: true,
|
|
34272
|
+
description: "Template-literal SQL query with ${...} interpolation \u2014 use parameterized queries.",
|
|
34273
|
+
create(context) {
|
|
34274
|
+
return context;
|
|
34275
|
+
},
|
|
34276
|
+
analyze(_context, facts) {
|
|
34277
|
+
const issues = [];
|
|
34278
|
+
const source = facts.v2?._source;
|
|
34279
|
+
if (!source) return issues;
|
|
34280
|
+
TEMPLATE_SQL_RE.lastIndex = 0;
|
|
34281
|
+
let m;
|
|
34282
|
+
while ((m = TEMPLATE_SQL_RE.exec(source)) !== null) {
|
|
34283
|
+
issues.push({
|
|
34284
|
+
ruleId: "db/sql-concat",
|
|
34285
|
+
category: "db",
|
|
34286
|
+
severity: "high",
|
|
34287
|
+
aiSpecific: true,
|
|
34288
|
+
message: "Template-literal SQL query with `${...}` interpolation \u2014 string concatenation is a SQL injection vector and a common AI-generated smell.",
|
|
34289
|
+
line: lineOfSource(source, m.index),
|
|
34290
|
+
column: 1,
|
|
34291
|
+
advice: "Use parameterized queries (`db.query('SELECT ... WHERE id = $1', [id])`) or an ORM."
|
|
34292
|
+
});
|
|
34293
|
+
}
|
|
34294
|
+
return issues;
|
|
34295
|
+
}
|
|
34296
|
+
});
|
|
34297
|
+
|
|
34298
|
+
// src/rules/docs/broken-link.ts
|
|
34299
|
+
var import_node_fs6 = require("fs");
|
|
34300
|
+
var import_node_path6 = require("path");
|
|
34301
|
+
|
|
34302
|
+
// src/engine/doc-freshness.ts
|
|
34303
|
+
var import_node_fs5 = require("fs");
|
|
34304
|
+
var import_node_path5 = require("path");
|
|
34305
|
+
var import_globby2 = require("globby");
|
|
34306
|
+
|
|
34307
|
+
// src/mcp/patterns.ts
|
|
34308
|
+
var import_node_fs3 = require("fs");
|
|
34309
|
+
var import_node_path3 = require("path");
|
|
34310
|
+
|
|
34311
|
+
// src/engine/discover.ts
|
|
34312
|
+
var import_globby = require("globby");
|
|
34313
|
+
var import_minimatch = require("minimatch");
|
|
34314
|
+
var import_node_path = require("path");
|
|
34315
|
+
var import_node_fs = require("fs");
|
|
34316
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte", ".astro", ".html"]);
|
|
34317
|
+
var BACKEND_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
34318
|
+
".py",
|
|
34319
|
+
".go",
|
|
34320
|
+
// v0.14.0
|
|
34321
|
+
".swift",
|
|
34322
|
+
".kt",
|
|
34323
|
+
".kts",
|
|
34324
|
+
".dart",
|
|
34325
|
+
".rs",
|
|
34326
|
+
".cpp",
|
|
34327
|
+
".cc",
|
|
34328
|
+
".cxx",
|
|
34329
|
+
".c",
|
|
34330
|
+
".h",
|
|
34331
|
+
".hpp",
|
|
34332
|
+
".hxx",
|
|
34333
|
+
".java",
|
|
34334
|
+
".rb",
|
|
34335
|
+
".php"
|
|
34336
|
+
]);
|
|
34337
|
+
var ALL_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
34338
|
+
...SOURCE_EXTENSIONS,
|
|
34339
|
+
...BACKEND_EXTENSIONS
|
|
34340
|
+
]);
|
|
34341
|
+
|
|
34342
|
+
// src/config/conventions.ts
|
|
34343
|
+
var import_node_fs2 = require("fs");
|
|
34344
|
+
var import_node_path2 = require("path");
|
|
34345
|
+
|
|
34346
|
+
// src/mcp/patterns.ts
|
|
34347
|
+
var ESM_IMPORT_RE = /(?:^|\n)\s*(?:import\s+(?:type\s+)?(?:[\w*${},\s]+\s+from\s+)?|import\s+|export\s+(?:type\s+)?[\w*${},\s]+\s+from\s+)(['"])([^'"]+)\1/g;
|
|
34348
|
+
var DYNAMIC_IMPORT_RE = /import\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
|
|
34349
|
+
var COMMONJS_REQUIRE_RE = /require\s*\(\s*(['"])([^'"]+)\1\s*\)/g;
|
|
34350
|
+
function extractImports(source) {
|
|
34351
|
+
const seen = /* @__PURE__ */ new Set();
|
|
34352
|
+
const out = [];
|
|
34353
|
+
const push = (spec) => {
|
|
34354
|
+
if (spec.startsWith(".") || spec.startsWith("/")) return;
|
|
34355
|
+
if (seen.has(spec)) return;
|
|
34356
|
+
seen.add(spec);
|
|
34357
|
+
out.push(spec);
|
|
34358
|
+
};
|
|
34359
|
+
for (const re of [ESM_IMPORT_RE, DYNAMIC_IMPORT_RE, COMMONJS_REQUIRE_RE]) {
|
|
34360
|
+
re.lastIndex = 0;
|
|
34361
|
+
let m;
|
|
34362
|
+
while ((m = re.exec(source)) !== null) {
|
|
34363
|
+
push(m[2]);
|
|
34364
|
+
}
|
|
34365
|
+
}
|
|
34366
|
+
return out;
|
|
34367
|
+
}
|
|
34368
|
+
|
|
34369
|
+
// src/rules/docs/expired-code-example.ts
|
|
34370
|
+
var CODE_LANGS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "javascript", "typescript"]);
|
|
34371
|
+
function stripSubpath(spec) {
|
|
34372
|
+
if (spec.startsWith("@")) return spec.split("/").slice(0, 2).join("/");
|
|
34373
|
+
return spec.split("/")[0] ?? spec;
|
|
34374
|
+
}
|
|
34375
|
+
var expiredCodeExampleRule = createRule({
|
|
34376
|
+
id: "docs/expired-code-example",
|
|
34377
|
+
category: "docs",
|
|
34378
|
+
severity: "medium",
|
|
34379
|
+
aiSpecific: false,
|
|
34380
|
+
description: "A fenced code example imports a package that is not declared in package.json.",
|
|
34381
|
+
create(context) {
|
|
34382
|
+
return { ...context, packages: declaredPackages(context.cwd) };
|
|
34383
|
+
},
|
|
34384
|
+
analyze(context, facts) {
|
|
34385
|
+
const issues = [];
|
|
34386
|
+
const source = facts.v2?._source;
|
|
34387
|
+
if (!source) return issues;
|
|
34388
|
+
const blocks = extractFencedCodeBlocks(source);
|
|
34389
|
+
for (const block of blocks) {
|
|
34390
|
+
if (!CODE_LANGS.has(block.lang)) continue;
|
|
34391
|
+
if (block.body.split("\n").length < 2) continue;
|
|
34392
|
+
const imports = extractImports(block.body);
|
|
34393
|
+
for (const imp of imports) {
|
|
34394
|
+
const pkgName = stripSubpath(imp);
|
|
34395
|
+
if (context.packages.has(pkgName)) continue;
|
|
34396
|
+
issues.push({
|
|
34397
|
+
ruleId: "docs/expired-code-example",
|
|
34398
|
+
category: "docs",
|
|
34399
|
+
severity: "medium",
|
|
34400
|
+
aiSpecific: false,
|
|
34401
|
+
message: `Code example imports \`${imp}\` but \`${pkgName}\` is not in package.json.`,
|
|
34402
|
+
line: block.line,
|
|
34403
|
+
column: block.column,
|
|
34404
|
+
advice: `Add \`${pkgName}\` to package.json or update the example.`
|
|
34405
|
+
});
|
|
34406
|
+
}
|
|
34407
|
+
}
|
|
34408
|
+
return issues;
|
|
34409
|
+
}
|
|
34410
|
+
});
|
|
34411
|
+
|
|
34412
|
+
// src/rules/docs/stale-function-reference.ts
|
|
34413
|
+
var import_node_fs4 = require("fs");
|
|
34414
|
+
var import_node_path4 = require("path");
|
|
34415
|
+
var RESERVED = /* @__PURE__ */ new Set([
|
|
34416
|
+
"true",
|
|
34417
|
+
"false",
|
|
34418
|
+
"null",
|
|
34419
|
+
"undefined",
|
|
34420
|
+
"this",
|
|
34421
|
+
"self",
|
|
34422
|
+
"get",
|
|
34423
|
+
"set",
|
|
34424
|
+
"init",
|
|
34425
|
+
"destroy",
|
|
34426
|
+
"value",
|
|
34427
|
+
"key",
|
|
34428
|
+
"id",
|
|
34429
|
+
"name",
|
|
34430
|
+
"data",
|
|
34431
|
+
"error",
|
|
34432
|
+
"info",
|
|
34433
|
+
"debug",
|
|
34434
|
+
"log",
|
|
34435
|
+
"warn",
|
|
34436
|
+
"type",
|
|
34437
|
+
"class",
|
|
34438
|
+
"function",
|
|
34439
|
+
"const",
|
|
34440
|
+
"let",
|
|
34441
|
+
"var",
|
|
34442
|
+
"return",
|
|
34443
|
+
"if",
|
|
34444
|
+
"else",
|
|
34445
|
+
"for",
|
|
34446
|
+
"while",
|
|
34447
|
+
"do",
|
|
34448
|
+
"switch",
|
|
34449
|
+
"case",
|
|
34450
|
+
"default",
|
|
34451
|
+
"break",
|
|
34452
|
+
"continue",
|
|
34453
|
+
"new",
|
|
34454
|
+
"delete",
|
|
34455
|
+
"try",
|
|
34456
|
+
"catch",
|
|
34457
|
+
"finally",
|
|
34458
|
+
"async",
|
|
34459
|
+
"await",
|
|
34460
|
+
"import",
|
|
34461
|
+
"export",
|
|
34462
|
+
"from",
|
|
34463
|
+
"as",
|
|
34464
|
+
"then",
|
|
34465
|
+
"resolve",
|
|
34466
|
+
"reject",
|
|
34467
|
+
"next",
|
|
34468
|
+
"prev",
|
|
34469
|
+
"current",
|
|
34470
|
+
"index",
|
|
34471
|
+
"count",
|
|
34472
|
+
"length",
|
|
34473
|
+
"size",
|
|
34474
|
+
"width",
|
|
34475
|
+
"height",
|
|
34476
|
+
"top",
|
|
34477
|
+
"left",
|
|
34478
|
+
"right",
|
|
34479
|
+
"bottom",
|
|
34480
|
+
"result",
|
|
34481
|
+
"response",
|
|
34482
|
+
"request",
|
|
34483
|
+
"user",
|
|
34484
|
+
"message",
|
|
34485
|
+
"code",
|
|
34486
|
+
"status",
|
|
34487
|
+
"state",
|
|
34488
|
+
"props",
|
|
34489
|
+
"ctx",
|
|
34490
|
+
"context",
|
|
34491
|
+
"config",
|
|
34492
|
+
"options",
|
|
34493
|
+
"params",
|
|
34494
|
+
"args",
|
|
34495
|
+
"event",
|
|
34496
|
+
"target",
|
|
34497
|
+
"input",
|
|
34498
|
+
"output",
|
|
34499
|
+
"src",
|
|
34500
|
+
"dest",
|
|
34501
|
+
"path",
|
|
34502
|
+
"file",
|
|
34503
|
+
"dir",
|
|
34504
|
+
"url",
|
|
34505
|
+
"header",
|
|
34506
|
+
"body",
|
|
34507
|
+
"token",
|
|
34508
|
+
"auth",
|
|
34509
|
+
"session",
|
|
34510
|
+
"react",
|
|
34511
|
+
"node",
|
|
34512
|
+
"next",
|
|
34513
|
+
"vue",
|
|
34514
|
+
"angular",
|
|
34515
|
+
"svelte"
|
|
34516
|
+
]);
|
|
34517
|
+
var SOURCE_EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
34518
|
+
var SOURCE_ROOTS = ["src", "lib", "app", "components"];
|
|
34519
|
+
var CAP = 200;
|
|
34520
|
+
function walk(dir, out, cap) {
|
|
34521
|
+
if (!(0, import_node_fs4.existsSync)(dir) || out.length >= cap) return;
|
|
34522
|
+
let entries;
|
|
34523
|
+
try {
|
|
34524
|
+
entries = (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true });
|
|
34525
|
+
} catch {
|
|
34526
|
+
return;
|
|
34527
|
+
}
|
|
34528
|
+
for (const entry of entries) {
|
|
34529
|
+
if (out.length >= cap) return;
|
|
34530
|
+
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
34531
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
34532
|
+
if (entry.isDirectory()) walk(full, out, cap);
|
|
34533
|
+
else if (entry.isFile() && SOURCE_EXTS.has((0, import_node_path4.extname)(entry.name))) out.push(full);
|
|
34534
|
+
}
|
|
34535
|
+
}
|
|
34536
|
+
function collectExports(cwd) {
|
|
34537
|
+
const out = /* @__PURE__ */ new Set();
|
|
34538
|
+
const files = [];
|
|
34539
|
+
for (const root of SOURCE_ROOTS) walk((0, import_node_path4.join)(cwd, root), files, CAP);
|
|
34540
|
+
for (const file of files) {
|
|
34541
|
+
let source;
|
|
34542
|
+
try {
|
|
34543
|
+
source = (0, import_node_fs4.readFileSync)(file, "utf-8");
|
|
34544
|
+
} catch {
|
|
34545
|
+
continue;
|
|
34546
|
+
}
|
|
34547
|
+
for (const re of [
|
|
34548
|
+
/\bexport\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
|
|
34549
|
+
/\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g,
|
|
34550
|
+
/\bexport\s+class\s+([A-Za-z_$][\w$]*)/g,
|
|
34551
|
+
/\bexport\s+interface\s+([A-Za-z_$][\w$]*)/g,
|
|
34552
|
+
/\bexport\s+type\s+([A-Za-z_$][\w$]*)/g,
|
|
34553
|
+
/\bexport\s+default\s+(?:function\s+|class\s+)?([A-Za-z_$][\w$]*)/g
|
|
34554
|
+
]) {
|
|
34555
|
+
let m;
|
|
34556
|
+
while ((m = re.exec(source)) !== null) {
|
|
34557
|
+
const name = m[1];
|
|
34558
|
+
if (name) out.add(name);
|
|
34559
|
+
}
|
|
34560
|
+
}
|
|
34561
|
+
}
|
|
34562
|
+
return out;
|
|
34563
|
+
}
|
|
34564
|
+
var staleFunctionReferenceRule = createRule({
|
|
34565
|
+
id: "docs/stale-function-reference",
|
|
34566
|
+
category: "docs",
|
|
34567
|
+
severity: "medium",
|
|
34568
|
+
aiSpecific: false,
|
|
34569
|
+
description: "Markdown references an identifier in a calling context (foo()) that is not exported by the project.",
|
|
34570
|
+
create(context) {
|
|
34571
|
+
return { ...context, exports: collectExports(context.cwd) };
|
|
34572
|
+
},
|
|
34573
|
+
analyze(context, facts) {
|
|
34574
|
+
const issues = [];
|
|
34575
|
+
const source = facts.v2?._source;
|
|
34576
|
+
if (!source) return issues;
|
|
34577
|
+
for (const span of extractInlineCodeSpans(source)) {
|
|
34578
|
+
const text = span.text;
|
|
34579
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(text)) continue;
|
|
34580
|
+
if (text.length < 3) continue;
|
|
34581
|
+
if (RESERVED.has(text.toLowerCase())) continue;
|
|
34582
|
+
if (context.exports.has(text)) continue;
|
|
34583
|
+
const end = Math.min(source.length, span.index + text.length + 50);
|
|
34584
|
+
if (!/\(/.test(source.slice(span.index, end))) continue;
|
|
34585
|
+
issues.push({
|
|
34586
|
+
ruleId: "docs/stale-function-reference",
|
|
34587
|
+
category: "docs",
|
|
34588
|
+
severity: "medium",
|
|
34589
|
+
aiSpecific: false,
|
|
34590
|
+
message: `Documents \`${text}()\` but no such export exists.`,
|
|
34591
|
+
line: span.line,
|
|
34592
|
+
column: span.column,
|
|
34593
|
+
advice: `Rename the doc reference, or add a \`${text}\` wrapper export.`,
|
|
34594
|
+
extras: { identifier: text }
|
|
34595
|
+
});
|
|
34596
|
+
}
|
|
34597
|
+
return issues;
|
|
34598
|
+
}
|
|
34599
|
+
});
|
|
34600
|
+
|
|
34601
|
+
// src/rules/docs/stale-package-reference.ts
|
|
34602
|
+
var ENGLISH_WORD_DENYLIST = /* @__PURE__ */ new Set([
|
|
34603
|
+
"the",
|
|
34604
|
+
"and",
|
|
34605
|
+
"for",
|
|
34606
|
+
"with",
|
|
34607
|
+
"from",
|
|
34608
|
+
"this",
|
|
34609
|
+
"that",
|
|
34610
|
+
"npm",
|
|
34611
|
+
"npx",
|
|
34612
|
+
"pnpm",
|
|
34613
|
+
"yarn",
|
|
34614
|
+
"node",
|
|
34615
|
+
"git",
|
|
34616
|
+
"cli",
|
|
34617
|
+
"api",
|
|
34618
|
+
"sdk",
|
|
34619
|
+
"src",
|
|
34620
|
+
"dist",
|
|
34621
|
+
"lib",
|
|
34622
|
+
"bin",
|
|
34623
|
+
"doc",
|
|
34624
|
+
"docs",
|
|
34625
|
+
"test",
|
|
34626
|
+
"spec",
|
|
34627
|
+
"todo",
|
|
34628
|
+
"fix",
|
|
34629
|
+
"bug",
|
|
34630
|
+
"feat",
|
|
34631
|
+
"refactor",
|
|
34632
|
+
"chore",
|
|
34633
|
+
"http",
|
|
34634
|
+
"https",
|
|
34635
|
+
"url",
|
|
34636
|
+
"json",
|
|
34637
|
+
"xml",
|
|
34638
|
+
"yaml",
|
|
34639
|
+
"sql",
|
|
34640
|
+
"orm",
|
|
34641
|
+
"css",
|
|
34642
|
+
"html",
|
|
34643
|
+
"svg",
|
|
34644
|
+
"png",
|
|
34645
|
+
"jpg",
|
|
34646
|
+
"pdf",
|
|
34647
|
+
"csv",
|
|
34648
|
+
"md",
|
|
34649
|
+
"mdx",
|
|
34650
|
+
"ts",
|
|
34651
|
+
"tsx",
|
|
34652
|
+
"js",
|
|
34653
|
+
"jsx",
|
|
34654
|
+
"ok",
|
|
34655
|
+
"no",
|
|
34656
|
+
"yes"
|
|
34657
|
+
]);
|
|
34658
|
+
var stalePackageReferenceRule = createRule({
|
|
34659
|
+
id: "docs/stale-package-reference",
|
|
34660
|
+
category: "docs",
|
|
34661
|
+
severity: "medium",
|
|
34662
|
+
aiSpecific: false,
|
|
34663
|
+
description: "Markdown references a package (npm install / from / require) that is not in package.json.",
|
|
34664
|
+
create(context) {
|
|
34665
|
+
return { ...context, packages: declaredPackages(context.cwd) };
|
|
34666
|
+
},
|
|
34667
|
+
analyze(context, facts) {
|
|
34668
|
+
const issues = [];
|
|
34669
|
+
const source = facts.v2?._source;
|
|
34670
|
+
if (!source) return issues;
|
|
34671
|
+
const spans = extractInlineCodeSpans(source);
|
|
34672
|
+
for (const span of spans) {
|
|
34673
|
+
const lineStart = source.lastIndexOf("\n", span.index) + 1;
|
|
34674
|
+
const lineEnd = source.indexOf("\n", span.index);
|
|
34675
|
+
const line = source.slice(lineStart, lineEnd === -1 ? source.length : lineEnd);
|
|
34676
|
+
let candidate;
|
|
34677
|
+
const installMatch = /(npm\s+install|pnpm\s+add|yarn\s+add)\s+([A-Za-z0-9_./@-]+)/i.exec(line);
|
|
34678
|
+
if (installMatch) candidate = installMatch[2];
|
|
34679
|
+
if (!candidate) {
|
|
34680
|
+
const fromMatch = /from\s+['"]([^'"]+)['"]/i.exec(line);
|
|
34681
|
+
if (fromMatch) candidate = fromMatch[1];
|
|
34682
|
+
}
|
|
34683
|
+
if (!candidate) {
|
|
34684
|
+
const requireMatch = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/i.exec(line);
|
|
34685
|
+
if (requireMatch) candidate = requireMatch[1];
|
|
34686
|
+
}
|
|
34687
|
+
if (!candidate) continue;
|
|
34688
|
+
let pkgName = candidate;
|
|
34689
|
+
if (pkgName.startsWith("@")) {
|
|
34690
|
+
pkgName = pkgName.split("/").slice(0, 2).join("/");
|
|
34691
|
+
} else {
|
|
34692
|
+
pkgName = pkgName.split("/")[0] ?? pkgName;
|
|
34693
|
+
}
|
|
34694
|
+
if (!/^@?[a-z][a-z0-9._/-]*$/.test(pkgName)) continue;
|
|
34695
|
+
if (pkgName.length < 2) continue;
|
|
34696
|
+
if (ENGLISH_WORD_DENYLIST.has(pkgName)) continue;
|
|
34697
|
+
if (context.packages.has(pkgName)) continue;
|
|
34698
|
+
issues.push({
|
|
34699
|
+
ruleId: "docs/stale-package-reference",
|
|
34700
|
+
category: "docs",
|
|
34701
|
+
severity: "medium",
|
|
34702
|
+
aiSpecific: false,
|
|
34703
|
+
message: `Documents \`${pkgName}\` but it is not in package.json.`,
|
|
34704
|
+
line: span.line,
|
|
34705
|
+
column: span.column,
|
|
34706
|
+
advice: `Add \`${pkgName}\` to package.json or update the doc to reference an installed package.`,
|
|
34707
|
+
extras: { package: pkgName }
|
|
34708
|
+
});
|
|
34709
|
+
}
|
|
34710
|
+
return issues;
|
|
34711
|
+
}
|
|
34712
|
+
});
|
|
34713
|
+
|
|
34714
|
+
// src/engine/doc-freshness.ts
|
|
34715
|
+
function extractInlineCodeSpans(source) {
|
|
34716
|
+
const hits = [];
|
|
34717
|
+
const re = /`([^`\n]+?)`/g;
|
|
34718
|
+
let m;
|
|
34719
|
+
while ((m = re.exec(source)) !== null) {
|
|
34720
|
+
const text = m[1] ?? "";
|
|
34721
|
+
const upTo = source.slice(0, m.index);
|
|
34722
|
+
const line = upTo.split("\n").length;
|
|
34723
|
+
const lastNl = upTo.lastIndexOf("\n");
|
|
34724
|
+
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
34725
|
+
hits.push({ text, line, column, index: m.index });
|
|
34726
|
+
}
|
|
34727
|
+
return hits;
|
|
34728
|
+
}
|
|
34729
|
+
function extractFencedCodeBlocks(source) {
|
|
34730
|
+
const blocks = [];
|
|
34731
|
+
const lines = source.split("\n");
|
|
34732
|
+
let i = 0;
|
|
34733
|
+
while (i < lines.length) {
|
|
34734
|
+
const line = lines[i] ?? "";
|
|
34735
|
+
const fenceMatch = /^```(\w*)\s*$/.exec(line);
|
|
34736
|
+
if (!fenceMatch) {
|
|
34737
|
+
i++;
|
|
34738
|
+
continue;
|
|
34739
|
+
}
|
|
34740
|
+
const lang = fenceMatch[1] ?? "";
|
|
34741
|
+
const startLine = i + 1;
|
|
34742
|
+
const bodyLines = [];
|
|
34743
|
+
i++;
|
|
34744
|
+
while (i < lines.length) {
|
|
34745
|
+
if (/^```\s*$/.test(lines[i] ?? "")) {
|
|
34746
|
+
i++;
|
|
34747
|
+
break;
|
|
34748
|
+
}
|
|
34749
|
+
bodyLines.push(lines[i] ?? "");
|
|
34750
|
+
i++;
|
|
34751
|
+
}
|
|
34752
|
+
blocks.push({
|
|
34753
|
+
lang,
|
|
34754
|
+
body: bodyLines.join("\n"),
|
|
34755
|
+
line: startLine,
|
|
34756
|
+
column: 1
|
|
34757
|
+
});
|
|
34758
|
+
}
|
|
34759
|
+
return blocks;
|
|
34760
|
+
}
|
|
34761
|
+
function extractMarkdownLinks(source) {
|
|
34762
|
+
const hits = [];
|
|
34763
|
+
const re = /(?<!\!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
|
|
34764
|
+
let m;
|
|
34765
|
+
while ((m = re.exec(source)) !== null) {
|
|
34766
|
+
const target = m[2] ?? "";
|
|
34767
|
+
const upTo = source.slice(0, m.index);
|
|
34768
|
+
const line = upTo.split("\n").length;
|
|
34769
|
+
const lastNl = upTo.lastIndexOf("\n");
|
|
34770
|
+
const column = lastNl === -1 ? m.index + 1 : m.index - lastNl;
|
|
34771
|
+
hits.push({ target, line, column });
|
|
34772
|
+
}
|
|
34773
|
+
return hits;
|
|
34774
|
+
}
|
|
34775
|
+
function declaredPackages(cwd) {
|
|
34776
|
+
const out = /* @__PURE__ */ new Set();
|
|
34777
|
+
const pkgPath = (0, import_node_path5.join)(cwd, "package.json");
|
|
34778
|
+
if (!(0, import_node_fs5.existsSync)(pkgPath)) return out;
|
|
34779
|
+
try {
|
|
34780
|
+
const raw = (0, import_node_fs5.readFileSync)(pkgPath, "utf-8");
|
|
34781
|
+
const pkg = JSON.parse(raw);
|
|
34782
|
+
for (const k of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
34783
|
+
const v = pkg[k];
|
|
34784
|
+
if (v && typeof v === "object") {
|
|
34785
|
+
for (const name of Object.keys(v)) {
|
|
34786
|
+
out.add(name);
|
|
34787
|
+
}
|
|
34788
|
+
}
|
|
34789
|
+
}
|
|
34790
|
+
} catch {
|
|
34791
|
+
}
|
|
34792
|
+
return out;
|
|
34793
|
+
}
|
|
34794
|
+
|
|
34795
|
+
// src/rules/docs/broken-link.ts
|
|
34796
|
+
var brokenLinkRule = createRule({
|
|
34797
|
+
id: "docs/broken-link",
|
|
34798
|
+
category: "docs",
|
|
34799
|
+
severity: "low",
|
|
34800
|
+
aiSpecific: false,
|
|
34801
|
+
description: "Markdown link target is relative and does not resolve to a file on disk.",
|
|
34802
|
+
create(context) {
|
|
34803
|
+
return context;
|
|
34804
|
+
},
|
|
34805
|
+
analyze(context, facts) {
|
|
34806
|
+
const issues = [];
|
|
34807
|
+
const source = facts.v2?._source;
|
|
34808
|
+
if (!source) return issues;
|
|
34809
|
+
const links = extractMarkdownLinks(source);
|
|
34810
|
+
const docDir = (0, import_node_path6.dirname)((0, import_node_path6.resolve)(context.cwd, context.filePath));
|
|
34811
|
+
for (const link of links) {
|
|
34812
|
+
const target = link.target;
|
|
34813
|
+
if (target.startsWith("http://") || target.startsWith("https://")) continue;
|
|
34814
|
+
if (target.startsWith("mailto:") || target.startsWith("tel:")) continue;
|
|
34815
|
+
if (target.startsWith("#")) continue;
|
|
34816
|
+
if (target.startsWith("//")) continue;
|
|
34817
|
+
if (target.startsWith("/")) continue;
|
|
34818
|
+
const resolved = (0, import_node_path6.join)(docDir, target);
|
|
34819
|
+
if ((0, import_node_fs6.existsSync)(resolved)) continue;
|
|
34820
|
+
issues.push({
|
|
34821
|
+
ruleId: "docs/broken-link",
|
|
34822
|
+
category: "docs",
|
|
34823
|
+
severity: "low",
|
|
34824
|
+
aiSpecific: false,
|
|
34825
|
+
message: `Relative link \`${target}\` does not exist.`,
|
|
34826
|
+
line: link.line,
|
|
34827
|
+
column: link.column,
|
|
34828
|
+
advice: `Create the file or fix the link target.`,
|
|
34829
|
+
extras: { link: target }
|
|
34830
|
+
});
|
|
34831
|
+
}
|
|
34832
|
+
return issues;
|
|
34833
|
+
}
|
|
34834
|
+
});
|
|
34835
|
+
|
|
33932
34836
|
// src/rules/layout/gap-monopoly.ts
|
|
33933
34837
|
var GAP_RE = /\bgap(?:-x|-y)?-(\d+)\b/g;
|
|
33934
34838
|
var gapMonopolyRule = createRule({
|
|
@@ -34209,6 +35113,7 @@ var DEFAULT_CONFIG = {
|
|
|
34209
35113
|
"wcag/dragging-movements": "medium",
|
|
34210
35114
|
"wcag/focus-appearance": "high",
|
|
34211
35115
|
"wcag/focus-obscured": "low",
|
|
35116
|
+
"wcag/missing-alt": "medium",
|
|
34212
35117
|
"wcag/target-size": "high",
|
|
34213
35118
|
"test/weak-assertion": "medium",
|
|
34214
35119
|
"test/duplicate-setup": "medium",
|
|
@@ -34280,16 +35185,16 @@ var DEFAULT_CONFIG = {
|
|
|
34280
35185
|
};
|
|
34281
35186
|
|
|
34282
35187
|
// src/config/detect/monorepo.ts
|
|
34283
|
-
var
|
|
34284
|
-
var
|
|
35188
|
+
var import_node_fs7 = require("fs");
|
|
35189
|
+
var import_node_path7 = require("path");
|
|
34285
35190
|
|
|
34286
35191
|
// src/config/detect/styling.ts
|
|
34287
|
-
var
|
|
34288
|
-
var
|
|
35192
|
+
var import_node_fs8 = require("fs");
|
|
35193
|
+
var import_node_path8 = require("path");
|
|
34289
35194
|
|
|
34290
35195
|
// src/config/detect/stack.ts
|
|
34291
|
-
var
|
|
34292
|
-
var
|
|
35196
|
+
var import_node_fs9 = require("fs");
|
|
35197
|
+
var import_node_path9 = require("path");
|
|
34293
35198
|
|
|
34294
35199
|
// src/config/presets.ts
|
|
34295
35200
|
var REACT_ONLY_RULES = {
|
|
@@ -34338,8 +35243,8 @@ var FRAMEWORK_PRESETS = {
|
|
|
34338
35243
|
};
|
|
34339
35244
|
|
|
34340
35245
|
// src/config/load.ts
|
|
34341
|
-
var
|
|
34342
|
-
var
|
|
35246
|
+
var import_node_fs10 = require("fs");
|
|
35247
|
+
var import_node_path10 = require("path");
|
|
34343
35248
|
var import_node_module = require("module");
|
|
34344
35249
|
|
|
34345
35250
|
// src/engine/logger.ts
|
|
@@ -34361,10 +35266,6 @@ function setLoggerQuiet(quiet) {
|
|
|
34361
35266
|
logger = createLogger(quiet);
|
|
34362
35267
|
}
|
|
34363
35268
|
|
|
34364
|
-
// src/config/conventions.ts
|
|
34365
|
-
var import_node_fs4 = require("fs");
|
|
34366
|
-
var import_node_path4 = require("path");
|
|
34367
|
-
|
|
34368
35269
|
// src/config/init.ts
|
|
34369
35270
|
var STRICTNESS_PRESETS = {
|
|
34370
35271
|
strict: {
|
|
@@ -34580,6 +35481,7 @@ var boundaryViolationRule = createRule({
|
|
|
34580
35481
|
category: "logic",
|
|
34581
35482
|
severity: "high",
|
|
34582
35483
|
aiSpecific: false,
|
|
35484
|
+
description: "Server-side data-layer / DB import leaked into a client component, or a client React hook used inside a server file (RSC boundary violation)",
|
|
34583
35485
|
create(ruleContext) {
|
|
34584
35486
|
return { clientHooks: CLIENT_HOOKS, supportsRsc: ruleContext.config.supportsRsc ?? true };
|
|
34585
35487
|
},
|
|
@@ -34809,7 +35711,11 @@ var mathAnyDensityRule = createRule({
|
|
|
34809
35711
|
category: "logic",
|
|
34810
35712
|
severity: "high",
|
|
34811
35713
|
aiSpecific: true,
|
|
34812
|
-
|
|
35714
|
+
// Self-match avoidance: the source uses string concatenation to keep the
|
|
35715
|
+
// literal ": any" sequence out of the regex match — the description
|
|
35716
|
+
// metadata is for humans, not the rule engine, so we keep it as a regular
|
|
35717
|
+
// string literal here.
|
|
35718
|
+
description: "`: any` density \u2265 3 per 100 lines \u2014 AI sprinkling of `any` types",
|
|
34813
35719
|
create(context) {
|
|
34814
35720
|
return context;
|
|
34815
35721
|
},
|
|
@@ -35763,6 +36669,94 @@ var dangerousCorsRule = createRule({
|
|
|
35763
36669
|
}
|
|
35764
36670
|
});
|
|
35765
36671
|
|
|
36672
|
+
// src/rules/security/eval.ts
|
|
36673
|
+
var EVAL_CALL_RE = /\beval\s*\(/g;
|
|
36674
|
+
var NEW_FUNCTION_RE = /\bnew\s+Function\s*\(/g;
|
|
36675
|
+
var QUALIFIED_EVAL_RE = /\b(?:window|globalThis|self|global)\s*\.\s*eval\s*\(/g;
|
|
36676
|
+
var BLOCK_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
36677
|
+
function scanForEvalCalls(source) {
|
|
36678
|
+
const stripped = source.replace(
|
|
36679
|
+
BLOCK_COMMENT_RE,
|
|
36680
|
+
(match) => match.replace(/[^\n]/g, " ")
|
|
36681
|
+
);
|
|
36682
|
+
const hits = [];
|
|
36683
|
+
let m;
|
|
36684
|
+
EVAL_CALL_RE.lastIndex = 0;
|
|
36685
|
+
while ((m = EVAL_CALL_RE.exec(stripped)) !== null) {
|
|
36686
|
+
const before = source.slice(0, m.index);
|
|
36687
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
36688
|
+
const lineStart = lastNewline + 1;
|
|
36689
|
+
const lineBeforeMatch = source.slice(lineStart, m.index);
|
|
36690
|
+
if (lineBeforeMatch.includes("//")) continue;
|
|
36691
|
+
const quoteCount = (lineBeforeMatch.match(/['"`]/g) || []).length;
|
|
36692
|
+
if (quoteCount % 2 === 1) continue;
|
|
36693
|
+
hits.push({
|
|
36694
|
+
message: "Avoid eval(): it is an RCE vector whenever the argument is or becomes attacker-controlled. Use JSON.parse, a sandboxed expression parser, or a precomputed dispatch table.",
|
|
36695
|
+
line: lineOfSource(stripped, m.index),
|
|
36696
|
+
column: m.index - lineStart + 1
|
|
36697
|
+
});
|
|
36698
|
+
}
|
|
36699
|
+
NEW_FUNCTION_RE.lastIndex = 0;
|
|
36700
|
+
while ((m = NEW_FUNCTION_RE.exec(stripped)) !== null) {
|
|
36701
|
+
const before = source.slice(0, m.index);
|
|
36702
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
36703
|
+
const lineStart = lastNewline + 1;
|
|
36704
|
+
const lineBeforeMatch = source.slice(lineStart, m.index);
|
|
36705
|
+
if (lineBeforeMatch.includes("//")) continue;
|
|
36706
|
+
const quoteCount = (lineBeforeMatch.match(/['"`]/g) || []).length;
|
|
36707
|
+
if (quoteCount % 2 === 1) continue;
|
|
36708
|
+
hits.push({
|
|
36709
|
+
message: "Avoid new Function(): the function-constructor evaluates a string at runtime \u2014 same RCE risk as eval().",
|
|
36710
|
+
line: lineOfSource(stripped, m.index),
|
|
36711
|
+
column: m.index - lineStart + 1
|
|
36712
|
+
});
|
|
36713
|
+
}
|
|
36714
|
+
QUALIFIED_EVAL_RE.lastIndex = 0;
|
|
36715
|
+
while ((m = QUALIFIED_EVAL_RE.exec(stripped)) !== null) {
|
|
36716
|
+
const before = source.slice(0, m.index);
|
|
36717
|
+
const lastNewline = before.lastIndexOf("\n");
|
|
36718
|
+
const lineStart = lastNewline + 1;
|
|
36719
|
+
const lineBeforeMatch = source.slice(lineStart, m.index);
|
|
36720
|
+
if (lineBeforeMatch.includes("//")) continue;
|
|
36721
|
+
const quoteCount = (lineBeforeMatch.match(/['"`]/g) || []).length;
|
|
36722
|
+
if (quoteCount % 2 === 1) continue;
|
|
36723
|
+
hits.push({
|
|
36724
|
+
message: "Avoid window.eval() / globalThis.eval() \u2014 same RCE risk as bare eval().",
|
|
36725
|
+
line: lineOfSource(stripped, m.index),
|
|
36726
|
+
column: m.index - lineStart + 1
|
|
36727
|
+
});
|
|
36728
|
+
}
|
|
36729
|
+
return hits;
|
|
36730
|
+
}
|
|
36731
|
+
var evalRule = createRule({
|
|
36732
|
+
id: "security/eval",
|
|
36733
|
+
category: "security",
|
|
36734
|
+
severity: "high",
|
|
36735
|
+
aiSpecific: false,
|
|
36736
|
+
description: "eval() / new Function() / window.eval() \u2014 RCE vector when the argument is attacker-controlled (OWASP A03:2021)",
|
|
36737
|
+
create(context) {
|
|
36738
|
+
return context;
|
|
36739
|
+
},
|
|
36740
|
+
analyze(_context, facts) {
|
|
36741
|
+
const issues = [];
|
|
36742
|
+
const source = facts.v2?._source;
|
|
36743
|
+
if (!source) return issues;
|
|
36744
|
+
for (const hit of scanForEvalCalls(source)) {
|
|
36745
|
+
issues.push({
|
|
36746
|
+
ruleId: "security/eval",
|
|
36747
|
+
category: "security",
|
|
36748
|
+
severity: "high",
|
|
36749
|
+
aiSpecific: false,
|
|
36750
|
+
message: hit.message,
|
|
36751
|
+
line: hit.line,
|
|
36752
|
+
column: hit.column,
|
|
36753
|
+
advice: "Replace eval() with JSON.parse, a sandboxed expression parser, or a precomputed dispatch table. If you must compile dynamic code, use a Web Worker with a strict CSP and no network access."
|
|
36754
|
+
});
|
|
36755
|
+
}
|
|
36756
|
+
return issues;
|
|
36757
|
+
}
|
|
36758
|
+
});
|
|
36759
|
+
|
|
35766
36760
|
// src/rules/security/exposed-env-var.ts
|
|
35767
36761
|
var CLIENT_PREFIXES = [
|
|
35768
36762
|
"NEXT_PUBLIC_",
|
|
@@ -35948,6 +36942,86 @@ var hardcodedSecretRule = createRule({
|
|
|
35948
36942
|
}
|
|
35949
36943
|
});
|
|
35950
36944
|
|
|
36945
|
+
// src/rules/security/localstorage-token.ts
|
|
36946
|
+
var LS_SETITEM_RE = /(?:localStorage|sessionStorage)\s*\.\s*setItem\s*\(\s*(['"`])([^'"`]+)\1/g;
|
|
36947
|
+
var LS_SETITEM_VAR_RE = /(?:localStorage|sessionStorage)\s*\.\s*setItem\s*\(\s*(?!['"`])([A-Za-z_$][\w$]*)/g;
|
|
36948
|
+
var TOKEN_KEY_RE = /(token|jwt|bearer|access|refresh|auth|session[_-]?id|session[_-]?token|id[_-]?token|sso|api[_-]?key)/i;
|
|
36949
|
+
var NON_TOKEN_KEY_ALLOWLIST = /* @__PURE__ */ new Set([
|
|
36950
|
+
"theme",
|
|
36951
|
+
"lang",
|
|
36952
|
+
"locale",
|
|
36953
|
+
"i18n",
|
|
36954
|
+
"user_pref",
|
|
36955
|
+
"userprefs",
|
|
36956
|
+
"prefs",
|
|
36957
|
+
"preferences",
|
|
36958
|
+
"settings",
|
|
36959
|
+
"ui_theme",
|
|
36960
|
+
"sidebar",
|
|
36961
|
+
"view_mode",
|
|
36962
|
+
"cart",
|
|
36963
|
+
"last_seen",
|
|
36964
|
+
"onboarding",
|
|
36965
|
+
"dismissed"
|
|
36966
|
+
]);
|
|
36967
|
+
function isTokenY(rawKey) {
|
|
36968
|
+
const k = rawKey.toLowerCase();
|
|
36969
|
+
return !NON_TOKEN_KEY_ALLOWLIST.has(k) && TOKEN_KEY_RE.test(k);
|
|
36970
|
+
}
|
|
36971
|
+
function pushIssue(out, source, offset, message, advice) {
|
|
36972
|
+
out.push({
|
|
36973
|
+
ruleId: "security/localstorage-token",
|
|
36974
|
+
category: "security",
|
|
36975
|
+
severity: "high",
|
|
36976
|
+
aiSpecific: false,
|
|
36977
|
+
message,
|
|
36978
|
+
advice,
|
|
36979
|
+
line: lineOfSource(source, offset),
|
|
36980
|
+
column: 1
|
|
36981
|
+
});
|
|
36982
|
+
}
|
|
36983
|
+
var localstorageTokenRule = createRule({
|
|
36984
|
+
id: "security/localstorage-token",
|
|
36985
|
+
category: "security",
|
|
36986
|
+
severity: "high",
|
|
36987
|
+
aiSpecific: false,
|
|
36988
|
+
description: "Auth token stored in localStorage \u2014 vulnerable to XSS exfiltration (OWASP A03:2021).",
|
|
36989
|
+
create(context) {
|
|
36990
|
+
return context;
|
|
36991
|
+
},
|
|
36992
|
+
analyze(_context, facts) {
|
|
36993
|
+
const issues = [];
|
|
36994
|
+
const source = facts.v2?._source ?? "";
|
|
36995
|
+
if (!source) return issues;
|
|
36996
|
+
let m;
|
|
36997
|
+
LS_SETITEM_RE.lastIndex = 0;
|
|
36998
|
+
while ((m = LS_SETITEM_RE.exec(source)) !== null) {
|
|
36999
|
+
const key = m[2];
|
|
37000
|
+
if (!isTokenY(key)) continue;
|
|
37001
|
+
pushIssue(
|
|
37002
|
+
issues,
|
|
37003
|
+
source,
|
|
37004
|
+
m.index,
|
|
37005
|
+
`Auth key '${key}' written to localStorage / sessionStorage. Any page script can read it (XSS, malicious dep, browser extension).`,
|
|
37006
|
+
"Issue the token as an httpOnly Secure SameSite cookie so JS cannot access it. Keep credentials out of client JS entirely."
|
|
37007
|
+
);
|
|
37008
|
+
}
|
|
37009
|
+
LS_SETITEM_VAR_RE.lastIndex = 0;
|
|
37010
|
+
while ((m = LS_SETITEM_VAR_RE.exec(source)) !== null) {
|
|
37011
|
+
const ident = m[1];
|
|
37012
|
+
if (!TOKEN_KEY_RE.test(ident)) continue;
|
|
37013
|
+
pushIssue(
|
|
37014
|
+
issues,
|
|
37015
|
+
source,
|
|
37016
|
+
m.index,
|
|
37017
|
+
`localStorage.setItem('${ident}', ...) \u2014 identifier '${ident}' looks token-y. Verify the value is not a credential.`,
|
|
37018
|
+
"Trace the value being persisted. Tokens must never reach localStorage; issue as httpOnly cookies and call them server-side only."
|
|
37019
|
+
);
|
|
37020
|
+
}
|
|
37021
|
+
return issues;
|
|
37022
|
+
}
|
|
37023
|
+
});
|
|
37024
|
+
|
|
35951
37025
|
// src/rules/security/missing-auth-check.ts
|
|
35952
37026
|
var AUTH_PRIMITIVES = [
|
|
35953
37027
|
"getServerSession",
|
|
@@ -36119,6 +37193,53 @@ var sqlConstructionRule = createRule({
|
|
|
36119
37193
|
}
|
|
36120
37194
|
});
|
|
36121
37195
|
|
|
37196
|
+
// src/rules/security/target-blank-no-noopener.ts
|
|
37197
|
+
var ANCHOR_TAG_RE = /<a\s+([^>]*?)>/gi;
|
|
37198
|
+
var TARGET_BLANK_RE = /\btarget\s*=\s*(["'])_blank\1/i;
|
|
37199
|
+
var REL_ATTR_RE = /\brel\s*=\s*["']([^"']*)["']/i;
|
|
37200
|
+
function hasSafeRel(attrs) {
|
|
37201
|
+
const m = attrs.match(REL_ATTR_RE);
|
|
37202
|
+
if (!m) return false;
|
|
37203
|
+
const tokens = m[1].trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
37204
|
+
return tokens.includes("noopener") || tokens.includes("noreferrer");
|
|
37205
|
+
}
|
|
37206
|
+
function pushIssue2(out, source, offset) {
|
|
37207
|
+
out.push({
|
|
37208
|
+
ruleId: "security/target-blank-no-noopener",
|
|
37209
|
+
category: "security",
|
|
37210
|
+
severity: "medium",
|
|
37211
|
+
aiSpecific: false,
|
|
37212
|
+
message: '<a target="_blank"> without rel="noopener" (or rel="noreferrer"). window.opener can navigate the originating tab \u2014 reverse tabnabbing.',
|
|
37213
|
+
advice: 'Add rel="noopener" to the <a>. rel="noreferrer" implies noopener and also strips the Referer header.',
|
|
37214
|
+
line: lineOfSource(source, offset),
|
|
37215
|
+
column: 1
|
|
37216
|
+
});
|
|
37217
|
+
}
|
|
37218
|
+
var targetBlankNoNoopenerRule = createRule({
|
|
37219
|
+
id: "security/target-blank-no-noopener",
|
|
37220
|
+
category: "security",
|
|
37221
|
+
severity: "medium",
|
|
37222
|
+
aiSpecific: false,
|
|
37223
|
+
description: 'Link with target="_blank" missing rel="noopener" \u2014 window.opener can navigate the opener tab (reverse tabnabbing, MDN).',
|
|
37224
|
+
create(context) {
|
|
37225
|
+
return context;
|
|
37226
|
+
},
|
|
37227
|
+
analyze(_context, facts) {
|
|
37228
|
+
const issues = [];
|
|
37229
|
+
const source = facts.v2?._source ?? "";
|
|
37230
|
+
if (!source) return issues;
|
|
37231
|
+
let m;
|
|
37232
|
+
ANCHOR_TAG_RE.lastIndex = 0;
|
|
37233
|
+
while ((m = ANCHOR_TAG_RE.exec(source)) !== null) {
|
|
37234
|
+
const attrs = m[1];
|
|
37235
|
+
if (!TARGET_BLANK_RE.test(attrs)) continue;
|
|
37236
|
+
if (hasSafeRel(attrs)) continue;
|
|
37237
|
+
pushIssue2(issues, source, m.index);
|
|
37238
|
+
}
|
|
37239
|
+
return issues;
|
|
37240
|
+
}
|
|
37241
|
+
});
|
|
37242
|
+
|
|
36122
37243
|
// src/rules/security/unsafe-html-render.ts
|
|
36123
37244
|
var DANGEROUS_RE = /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([\s\S]+?)\}\s*\}\s*[\s/]/g;
|
|
36124
37245
|
function isStaticLiteral(value) {
|
|
@@ -36539,7 +37660,7 @@ function looksRealistic(value) {
|
|
|
36539
37660
|
|
|
36540
37661
|
// src/rules/test/missing-edge-case.ts
|
|
36541
37662
|
var import_core3 = require("@swc/core");
|
|
36542
|
-
var
|
|
37663
|
+
var import_node_fs11 = require("fs");
|
|
36543
37664
|
var MAX_PER_FILE = 20;
|
|
36544
37665
|
var missingEdgeCaseRule = createRule({
|
|
36545
37666
|
id: "test/missing-edge-case",
|
|
@@ -36561,7 +37682,7 @@ var missingEdgeCaseRule = createRule({
|
|
|
36561
37682
|
const testFileSources = /* @__PURE__ */ new Map();
|
|
36562
37683
|
for (const testFile of discoverTestFiles(cwd)) {
|
|
36563
37684
|
try {
|
|
36564
|
-
testFileSources.set(testFile, (0,
|
|
37685
|
+
testFileSources.set(testFile, (0, import_node_fs11.readFileSync)(testFile, "utf-8"));
|
|
36565
37686
|
} catch {
|
|
36566
37687
|
}
|
|
36567
37688
|
}
|
|
@@ -36815,16 +37936,16 @@ function discoverTestFiles(cwd) {
|
|
|
36815
37936
|
const found = [];
|
|
36816
37937
|
for (const root of roots) {
|
|
36817
37938
|
const abs = `${cwd}/${root}`;
|
|
36818
|
-
if (!(0,
|
|
36819
|
-
|
|
37939
|
+
if (!(0, import_node_fs11.existsSync)(abs)) continue;
|
|
37940
|
+
walk2(abs, found, import_node_fs11.readdirSync, import_node_fs11.statSync);
|
|
36820
37941
|
if (found.length > 200) break;
|
|
36821
37942
|
}
|
|
36822
37943
|
return found;
|
|
36823
37944
|
}
|
|
36824
|
-
function
|
|
37945
|
+
function walk2(dir, out, readdirSync6, statSync5) {
|
|
36825
37946
|
let entries;
|
|
36826
37947
|
try {
|
|
36827
|
-
entries =
|
|
37948
|
+
entries = readdirSync6(dir);
|
|
36828
37949
|
} catch {
|
|
36829
37950
|
return;
|
|
36830
37951
|
}
|
|
@@ -36838,7 +37959,7 @@ function walk(dir, out, readdirSync5, statSync5) {
|
|
|
36838
37959
|
continue;
|
|
36839
37960
|
}
|
|
36840
37961
|
if (stat.isDirectory()) {
|
|
36841
|
-
|
|
37962
|
+
walk2(full, out, readdirSync6, statSync5);
|
|
36842
37963
|
} else if (stat.isFile()) {
|
|
36843
37964
|
if (/\.(test|spec)\.[jt]sx?$/.test(entry) || /\.stories\.[jt]sx?$/.test(entry)) {
|
|
36844
37965
|
out.push(full);
|
|
@@ -37212,12 +38333,81 @@ var mathCtaVocabularyRule = createRule({
|
|
|
37212
38333
|
}
|
|
37213
38334
|
});
|
|
37214
38335
|
|
|
38336
|
+
// src/rules/typo/placeholder-text.ts
|
|
38337
|
+
var BAD_PATTERNS = [
|
|
38338
|
+
/\blorem\s+ipsum\b/i,
|
|
38339
|
+
/\bplaceholder\b/i,
|
|
38340
|
+
/\b(todo|fixme|xxx|aaa)\b/i,
|
|
38341
|
+
/\benter\s+text\b/i,
|
|
38342
|
+
/\btype\s+here\b/i,
|
|
38343
|
+
/\byour\s+text\s+here\b/i,
|
|
38344
|
+
/\bclick\s+here\b/i,
|
|
38345
|
+
/\b(asdf|qwerty)\b/i,
|
|
38346
|
+
/^(test|foo|bar|baz|sample)$/i
|
|
38347
|
+
];
|
|
38348
|
+
function isBadPlaceholder(value, config) {
|
|
38349
|
+
const trimmed = value.trim();
|
|
38350
|
+
if (config.allowlist && config.allowlist.includes(trimmed)) return false;
|
|
38351
|
+
const minLength = config.minLength ?? 3;
|
|
38352
|
+
if (trimmed.length < minLength) return false;
|
|
38353
|
+
return BAD_PATTERNS.some((re) => re.test(trimmed));
|
|
38354
|
+
}
|
|
38355
|
+
function scanForBadPlaceholders(source, config) {
|
|
38356
|
+
const hits = [];
|
|
38357
|
+
const seen = /* @__PURE__ */ new Set();
|
|
38358
|
+
const re = /placeholder\s*=\s*(?:"([^"]*)"|'([^']*)'|\{\s*"([^"]*)"\s*\}|\{\s*'([^']*)'\s*\})/gi;
|
|
38359
|
+
let m;
|
|
38360
|
+
while ((m = re.exec(source)) !== null) {
|
|
38361
|
+
if (seen.has(m.index)) continue;
|
|
38362
|
+
seen.add(m.index);
|
|
38363
|
+
const value = m[1] ?? m[2] ?? m[3] ?? m[4] ?? "";
|
|
38364
|
+
if (!isBadPlaceholder(value, config)) continue;
|
|
38365
|
+
hits.push({
|
|
38366
|
+
message: `Placeholder text "${value}" is a dev/AI default. Replace with real copy describing the expected input (e.g. "Search products", "Email address").`,
|
|
38367
|
+
line: lineOfSource(source, m.index),
|
|
38368
|
+
column: m.index - source.lastIndexOf("\n", m.index - 1),
|
|
38369
|
+
value
|
|
38370
|
+
});
|
|
38371
|
+
}
|
|
38372
|
+
return hits;
|
|
38373
|
+
}
|
|
38374
|
+
var placeholderTextRule = createRule({
|
|
38375
|
+
id: "typo/placeholder-text",
|
|
38376
|
+
category: "typo",
|
|
38377
|
+
severity: "low",
|
|
38378
|
+
aiSpecific: false,
|
|
38379
|
+
description: "Placeholder text contains dev/AI defaults (Lorem ipsum, Enter text here, TODO, etc.) \u2014 unfinished UI.",
|
|
38380
|
+
create(context) {
|
|
38381
|
+
return context;
|
|
38382
|
+
},
|
|
38383
|
+
analyze(context, facts) {
|
|
38384
|
+
const issues = [];
|
|
38385
|
+
const source = facts.v2?._source;
|
|
38386
|
+
if (!source) return issues;
|
|
38387
|
+
const config = context.config.ruleConfig["typo/placeholder-text"] ?? {};
|
|
38388
|
+
for (const hit of scanForBadPlaceholders(source, config)) {
|
|
38389
|
+
issues.push({
|
|
38390
|
+
ruleId: "typo/placeholder-text",
|
|
38391
|
+
category: "typo",
|
|
38392
|
+
severity: "low",
|
|
38393
|
+
aiSpecific: false,
|
|
38394
|
+
message: hit.message,
|
|
38395
|
+
line: hit.line,
|
|
38396
|
+
column: hit.column,
|
|
38397
|
+
advice: "Replace with specific, user-facing copy."
|
|
38398
|
+
});
|
|
38399
|
+
}
|
|
38400
|
+
return issues;
|
|
38401
|
+
}
|
|
38402
|
+
});
|
|
38403
|
+
|
|
37215
38404
|
// src/rules/visual/arbitrary-escape.ts
|
|
37216
38405
|
var arbitraryEscapeRule = createRule({
|
|
37217
38406
|
id: "visual/arbitrary-escape",
|
|
37218
38407
|
category: "visual",
|
|
37219
38408
|
severity: "medium",
|
|
37220
38409
|
aiSpecific: true,
|
|
38410
|
+
description: "Bracket-notation Tailwind values (e.g. `p-[13px]`, `bg-[#7c3aed]`) \u2014 AI agents reach for arbitrary escapes instead of design tokens (Refactoring UI; M\xE4ntyl\xE4 2003)",
|
|
37221
38411
|
create(context) {
|
|
37222
38412
|
return {
|
|
37223
38413
|
allowlist: context.config.arbitraryValueAllowlist
|
|
@@ -38084,6 +39274,7 @@ var focusAppearanceRule = createRule({
|
|
|
38084
39274
|
category: "wcag",
|
|
38085
39275
|
severity: "high",
|
|
38086
39276
|
aiSpecific: false,
|
|
39277
|
+
description: "Inject a global focus-ring CSS block (`*:focus-visible { outline: ... }`) \u2014 WCAG 2.4.7 (Focus Visible)",
|
|
38087
39278
|
create(context) {
|
|
38088
39279
|
return { globalCssTarget: context.config.globalCssTarget };
|
|
38089
39280
|
},
|
|
@@ -38160,6 +39351,55 @@ var focusObscuredRule = createRule({
|
|
|
38160
39351
|
}
|
|
38161
39352
|
});
|
|
38162
39353
|
|
|
39354
|
+
// src/rules/wcag/missing-alt.ts
|
|
39355
|
+
var IMG_OPEN_RE = /<img\b[^>]*>/gi;
|
|
39356
|
+
var HAS_ALT_RE = /\balt\s*=\s*("[^"]*"|'[^']*')/i;
|
|
39357
|
+
var HAS_PRESENTATION_ROLE_RE = /\brole\s*=\s*["'](?:presentation|none)["']/i;
|
|
39358
|
+
function scanForMissingAlt(source) {
|
|
39359
|
+
const hits = [];
|
|
39360
|
+
IMG_OPEN_RE.lastIndex = 0;
|
|
39361
|
+
let m;
|
|
39362
|
+
while ((m = IMG_OPEN_RE.exec(source)) !== null) {
|
|
39363
|
+
const tagText = m[0];
|
|
39364
|
+
if (HAS_ALT_RE.test(tagText)) continue;
|
|
39365
|
+
if (HAS_PRESENTATION_ROLE_RE.test(tagText)) continue;
|
|
39366
|
+
hits.push({
|
|
39367
|
+
message: '<img> element is missing an `alt` attribute. WCAG 2.1 SC 1.1.1 requires alt text for non-text content. Use alt="" for purely decorative images, or role="presentation" to remove it from the accessibility tree.',
|
|
39368
|
+
line: lineOfSource(source, m.index),
|
|
39369
|
+
column: m.index - source.lastIndexOf("\n", m.index - 1)
|
|
39370
|
+
});
|
|
39371
|
+
}
|
|
39372
|
+
return hits;
|
|
39373
|
+
}
|
|
39374
|
+
var missingAltRule = createRule({
|
|
39375
|
+
id: "wcag/missing-alt",
|
|
39376
|
+
category: "wcag",
|
|
39377
|
+
severity: "medium",
|
|
39378
|
+
aiSpecific: false,
|
|
39379
|
+
description: "<img> element is missing an `alt` attribute (WCAG 2.1 SC 1.1.1, Level A).",
|
|
39380
|
+
create(context) {
|
|
39381
|
+
return context;
|
|
39382
|
+
},
|
|
39383
|
+
analyze(_context, facts) {
|
|
39384
|
+
const issues = [];
|
|
39385
|
+
const source = facts.v2?._source;
|
|
39386
|
+
if (!source) return issues;
|
|
39387
|
+
for (const hit of scanForMissingAlt(source)) {
|
|
39388
|
+
issues.push({
|
|
39389
|
+
ruleId: "wcag/missing-alt",
|
|
39390
|
+
category: "wcag",
|
|
39391
|
+
severity: "medium",
|
|
39392
|
+
aiSpecific: false,
|
|
39393
|
+
message: hit.message,
|
|
39394
|
+
line: hit.line,
|
|
39395
|
+
column: hit.column,
|
|
39396
|
+
advice: 'Add `alt="..."` describing the image, or `alt=""` (or role="presentation") for purely decorative images.'
|
|
39397
|
+
});
|
|
39398
|
+
}
|
|
39399
|
+
return issues;
|
|
39400
|
+
}
|
|
39401
|
+
});
|
|
39402
|
+
|
|
38163
39403
|
// src/rules/wcag/target-size.ts
|
|
38164
39404
|
var SIZE_PREFIX_RE = /^(min-w|min-h|max-w|max-h|h|w|size)-(.+)$/;
|
|
38165
39405
|
var PAD_PREFIX_RE = /^(p|px|py)-(.+)$/;
|
|
@@ -38341,6 +39581,16 @@ var builtinRules = [
|
|
|
38341
39581
|
multipleComponentsPerFileRule,
|
|
38342
39582
|
shadcnPropMismatchRule,
|
|
38343
39583
|
importPathMismatchRule,
|
|
39584
|
+
duplicateIndexRule,
|
|
39585
|
+
enumSprawlRule,
|
|
39586
|
+
missingFkIndexRule,
|
|
39587
|
+
missingNotNullRule,
|
|
39588
|
+
namingInconsistencyRule,
|
|
39589
|
+
sqlConcatRule,
|
|
39590
|
+
brokenLinkRule,
|
|
39591
|
+
expiredCodeExampleRule,
|
|
39592
|
+
staleFunctionReferenceRule,
|
|
39593
|
+
stalePackageReferenceRule,
|
|
38344
39594
|
gapMonopolyRule,
|
|
38345
39595
|
mathElementUniformityRule,
|
|
38346
39596
|
mathGridUniformityRule,
|
|
@@ -38366,12 +39616,15 @@ var builtinRules = [
|
|
|
38366
39616
|
terminologyDriftRule,
|
|
38367
39617
|
uxPatternFragmentationRule,
|
|
38368
39618
|
dangerousCorsRule,
|
|
39619
|
+
evalRule,
|
|
38369
39620
|
exposedEnvVarRule,
|
|
38370
39621
|
failOpenAuthRule,
|
|
38371
39622
|
hardcodedSecretRule,
|
|
39623
|
+
localstorageTokenRule,
|
|
38372
39624
|
missingAuthCheckRule,
|
|
38373
39625
|
publicAdminRouteRule,
|
|
38374
39626
|
sqlConstructionRule,
|
|
39627
|
+
targetBlankNoNoopenerRule,
|
|
38375
39628
|
unsafeHtmlRenderRule,
|
|
38376
39629
|
duplicateSetupRule,
|
|
38377
39630
|
fakePlaceholderRule,
|
|
@@ -38382,6 +39635,7 @@ var builtinRules = [
|
|
|
38382
39635
|
clampOffscaleRule,
|
|
38383
39636
|
mathButtonLabelUniformityRule,
|
|
38384
39637
|
mathCtaVocabularyRule,
|
|
39638
|
+
placeholderTextRule,
|
|
38385
39639
|
arbitraryEscapeRule,
|
|
38386
39640
|
clampSoupRule,
|
|
38387
39641
|
forcedLayoutRule,
|
|
@@ -38399,6 +39653,7 @@ var builtinRules = [
|
|
|
38399
39653
|
draggingMovementsRule,
|
|
38400
39654
|
focusAppearanceRule,
|
|
38401
39655
|
focusObscuredRule,
|
|
39656
|
+
missingAltRule,
|
|
38402
39657
|
targetSizeRule
|
|
38403
39658
|
];
|
|
38404
39659
|
|
|
@@ -39192,6 +40447,156 @@ var signal_strength_default = {
|
|
|
39192
40447
|
lastCalibratedAt: "2026-06-27T12:00:00Z",
|
|
39193
40448
|
verdict: "USEFUL",
|
|
39194
40449
|
_calibrationNote: "v7 corpus re-calibration (2026-06-27, min-date=2025-01-01): 184488 neg + 239054 pos. USEFUL \u2014 TP=1912, FP=175, P=91.6%, FPR=0.09%, lift=8.4. aiSpecific=True."
|
|
40450
|
+
},
|
|
40451
|
+
"db/missing-fk-index": {
|
|
40452
|
+
recall: 0,
|
|
40453
|
+
fpRate: 0,
|
|
40454
|
+
ratio: 0,
|
|
40455
|
+
precision: 0,
|
|
40456
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40457
|
+
verdict: "DORMANT",
|
|
40458
|
+
defaultOff: true,
|
|
40459
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: PostgreSQL Global Development Group (2024), *PostgreSQL 16 Documentation \xA75.4 (Constraints)*, https://www.postgresql.org/docs/16/ddl-constraints.html; Squawk (2023), *Postgres linter rules*, https://github.com/sqllabs/squawk, rule `require-index-for-fk`. (Foreign key columns need a matching index \u2014 sequential scan on parent delete is a canonical Postgres anti-pattern.)"
|
|
40460
|
+
},
|
|
40461
|
+
"db/duplicate-index": {
|
|
40462
|
+
recall: 0,
|
|
40463
|
+
fpRate: 0,
|
|
40464
|
+
ratio: 0,
|
|
40465
|
+
precision: 0,
|
|
40466
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40467
|
+
verdict: "DORMANT",
|
|
40468
|
+
defaultOff: true,
|
|
40469
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: PostgreSQL Global Development Group (2024), *PostgreSQL 16 Documentation \xA711.2 (Index Types)*; Heroku Postgres Team (2018), *Efficient Use of PostgreSQL Indexes*. (Duplicate indexes silently double write cost without read benefit \u2014 Postgres does not warn.)"
|
|
40470
|
+
},
|
|
40471
|
+
"db/missing-not-null": {
|
|
40472
|
+
recall: 0,
|
|
40473
|
+
fpRate: 0,
|
|
40474
|
+
ratio: 0,
|
|
40475
|
+
precision: 0,
|
|
40476
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40477
|
+
verdict: "DORMANT",
|
|
40478
|
+
defaultOff: true,
|
|
40479
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: PostgreSQL Global Development Group (2024), *PostgreSQL 16 Documentation \xA75.4.1 (Check Constraints)*; Kleppmann, M. (2017), *Designing Data-Intensive Applications*, O'Reilly, ch.4 (NULL semantics in RDBMS). (Required-identifier columns without NOT NULL produce silent NULL inserts \u2014 canonical AI SQL smell.)"
|
|
40480
|
+
},
|
|
40481
|
+
"db/enum-sprawl": {
|
|
40482
|
+
recall: 0,
|
|
40483
|
+
fpRate: 0,
|
|
40484
|
+
ratio: 0,
|
|
40485
|
+
precision: 0,
|
|
40486
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40487
|
+
verdict: "DORMANT",
|
|
40488
|
+
defaultOff: true,
|
|
40489
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: PostgreSQL Global Development Group (2024), *PostgreSQL 16 Documentation \xA78.7 (Enumerated Types)*; Borkowski, A. (2022), *PostgreSQL enum types: when to use them and when not to*, pgconfig.org. (Enums with >12 values are brittle to extend and hard to localize \u2014 lookup table is the standard replacement.)"
|
|
40490
|
+
},
|
|
40491
|
+
"db/naming-inconsistency": {
|
|
40492
|
+
recall: 0,
|
|
40493
|
+
fpRate: 0,
|
|
40494
|
+
ratio: 0,
|
|
40495
|
+
precision: 0,
|
|
40496
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40497
|
+
verdict: "DORMANT",
|
|
40498
|
+
defaultOff: true,
|
|
40499
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: PostgreSQL Global Development Group (2024), *PostgreSQL 16 Documentation \xA74.1.1 (Identifiers and Key Words)*; pgsql-hackers (2018), *Re: snake_case vs camelCase in Postgres identifiers*. (snake_case is the Postgres convention; mixing styles in the same schema breaks ORM generators.)"
|
|
40500
|
+
},
|
|
40501
|
+
"db/sql-concat": {
|
|
40502
|
+
recall: 0,
|
|
40503
|
+
fpRate: 0,
|
|
40504
|
+
ratio: 0,
|
|
40505
|
+
precision: 0,
|
|
40506
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40507
|
+
verdict: "DORMANT",
|
|
40508
|
+
defaultOff: true,
|
|
40509
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: OWASP Foundation (2021), *OWASP Top 10 \u2014 A03:2021 Injection*, https://owasp.org/Top10/A03_2021-Injection/; OWASP Foundation (2017), *SQL Injection Prevention Cheat Sheet*. (Template-literal SQL with ${...} interpolation is the #1 SQL injection vector in AI-generated TypeScript code.)"
|
|
40510
|
+
},
|
|
40511
|
+
"docs/stale-package-reference": {
|
|
40512
|
+
recall: 0,
|
|
40513
|
+
fpRate: 0,
|
|
40514
|
+
ratio: 0,
|
|
40515
|
+
precision: 0,
|
|
40516
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40517
|
+
verdict: "DORMANT",
|
|
40518
|
+
defaultOff: true,
|
|
40519
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: Meng, M. et al. (2020), *The Cost of Fixing Code Documentation: An Empirical Study on Open-Source Software*, ICSE 2020; Aghajani, E. et al. (2019), *Software documentation: a practitioners' perspective*, ICSE 2019. (Doc references to undeclared packages are copy-paste rot from previous projects \u2014 high-signal doc drift.)"
|
|
40520
|
+
},
|
|
40521
|
+
"docs/stale-function-reference": {
|
|
40522
|
+
recall: 0,
|
|
40523
|
+
fpRate: 0,
|
|
40524
|
+
ratio: 0,
|
|
40525
|
+
precision: 0,
|
|
40526
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40527
|
+
verdict: "DORMANT",
|
|
40528
|
+
defaultOff: true,
|
|
40529
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: Meng, M. et al. (2020), *The Cost of Fixing Code Documentation*; Chen, Z. et al. (2022), *An Empirical Study on Code Documentation Adequacy*, MSR 2022. (Doc callouts to non-existent exports \u2014 readers copy-paste and hit a runtime error.)"
|
|
40530
|
+
},
|
|
40531
|
+
"docs/expired-code-example": {
|
|
40532
|
+
recall: 0,
|
|
40533
|
+
fpRate: 0,
|
|
40534
|
+
ratio: 0,
|
|
40535
|
+
precision: 0,
|
|
40536
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40537
|
+
verdict: "DORMANT",
|
|
40538
|
+
defaultOff: true,
|
|
40539
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: Parnas, D.L. (2010), *Precise Documentation: The Key to Better Software*, Springer (canonical doc-correctness argument); Chen, Z. et al. (2022). (Fenced code examples with broken imports erode trust in the entire docs site.)"
|
|
40540
|
+
},
|
|
40541
|
+
"docs/broken-link": {
|
|
40542
|
+
recall: 0,
|
|
40543
|
+
fpRate: 0,
|
|
40544
|
+
ratio: 0,
|
|
40545
|
+
precision: 0,
|
|
40546
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40547
|
+
verdict: "DORMANT",
|
|
40548
|
+
defaultOff: true,
|
|
40549
|
+
_calibrationNote: "v0.17.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: Nielsen, J. (2000), *Designing Web Usability*, New Riders (broken links as a top-5 trust erosion signal); Google Search Central (2024), *Crawl errors documentation*, https://developers.google.com/search/docs/crawling-indexing. (Broken internal links cost SEO crawl budget and reader trust.)"
|
|
40550
|
+
},
|
|
40551
|
+
"security/eval": {
|
|
40552
|
+
recall: 0,
|
|
40553
|
+
fpRate: 0,
|
|
40554
|
+
ratio: 0,
|
|
40555
|
+
precision: 0,
|
|
40556
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40557
|
+
verdict: "DORMANT",
|
|
40558
|
+
defaultOff: true,
|
|
40559
|
+
_calibrationNote: "v0.16.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: OWASP Foundation (2021), *OWASP Top 10 \u2014 A03:2021 Injection*; Mozilla Developer Network (2024), *eval() \u2014 MDN Web Docs*, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval (Security Considerations). (eval() and new Function() are RCE vectors if the input is ever attacker-controlled.)"
|
|
40560
|
+
},
|
|
40561
|
+
"security/localstorage-token": {
|
|
40562
|
+
recall: 0,
|
|
40563
|
+
fpRate: 0,
|
|
40564
|
+
ratio: 0,
|
|
40565
|
+
precision: 0,
|
|
40566
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40567
|
+
verdict: "DORMANT",
|
|
40568
|
+
defaultOff: true,
|
|
40569
|
+
_calibrationNote: "v0.16.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: OWASP Foundation (2021), *OWASP Top 10 \u2014 A07:2021 Identification and Authentication Failures*; OWASP Foundation (2022), *HTML5 Security Cheat Sheet \xA72.1 (Local Storage)*. (JWT/access/refresh tokens in localStorage are readable by any XSS payload \u2014 issue as httpOnly cookies instead.)"
|
|
40570
|
+
},
|
|
40571
|
+
"security/target-blank-no-noopener": {
|
|
40572
|
+
recall: 0,
|
|
40573
|
+
fpRate: 0,
|
|
40574
|
+
ratio: 0,
|
|
40575
|
+
precision: 0,
|
|
40576
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40577
|
+
verdict: "DORMANT",
|
|
40578
|
+
defaultOff: true,
|
|
40579
|
+
_calibrationNote: 'v0.16.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: OWASP Foundation (2022), *HTML5 Security Cheat Sheet \xA72.3 (Tabnabbing)*; WHATWG (2024), *HTML Living Standard \u2014 Link types: noopener*. (target="_blank" without rel="noopener" lets the linked page control window.opener \u2014 reverse tabnabbing.)'
|
|
40580
|
+
},
|
|
40581
|
+
"wcag/missing-alt": {
|
|
40582
|
+
recall: 0,
|
|
40583
|
+
fpRate: 0,
|
|
40584
|
+
ratio: 0,
|
|
40585
|
+
precision: 0,
|
|
40586
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40587
|
+
verdict: "DORMANT",
|
|
40588
|
+
defaultOff: true,
|
|
40589
|
+
_calibrationNote: 'v0.16.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: W3C (2023), *Web Content Accessibility Guidelines (WCAG) 2.2 \u2014 1.1.1 Non-text Content*, https://www.w3.org/TR/WCAG22/. (Every <img> needs alt text. Decorative: alt="". Informative: describe the image.)'
|
|
40590
|
+
},
|
|
40591
|
+
"typo/placeholder-text": {
|
|
40592
|
+
recall: 0,
|
|
40593
|
+
fpRate: 0,
|
|
40594
|
+
ratio: 0,
|
|
40595
|
+
precision: 0,
|
|
40596
|
+
lastCalibratedAt: "2026-06-30T00:00:00Z",
|
|
40597
|
+
verdict: "DORMANT",
|
|
40598
|
+
defaultOff: true,
|
|
40599
|
+
_calibrationNote: "v0.16.0 ship \u2014 not in v7 per-rule table. Default-off until calibration data lands. Backed by: Nielsen, J. (2000), *Designing Web Usability* (placeholder copy as a top-10 trust erosion signal); Krug, S. (2000), *Don't Make Me Think*, 2nd ed. (TODO/placeholder text in shipped UI signals incomplete work.)"
|
|
39195
40600
|
}
|
|
39196
40601
|
};
|
|
39197
40602
|
|
|
@@ -39217,7 +40622,7 @@ function applyRuleOverrides(issues, rules) {
|
|
|
39217
40622
|
return result;
|
|
39218
40623
|
}
|
|
39219
40624
|
async function scanFile(filePath, config, registry, cwd = process.cwd()) {
|
|
39220
|
-
const ext = (0,
|
|
40625
|
+
const ext = (0, import_node_path11.extname)(filePath).toLowerCase();
|
|
39221
40626
|
const UNSUPPORTED_LANGS = /* @__PURE__ */ new Set([
|
|
39222
40627
|
".swift",
|
|
39223
40628
|
".kt",
|