@geonosis/oxlint-plugin-biological-architecture 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RULES.md +35 -16
- package/corpus/.oxlintrc.json +43 -9
- package/corpus/affordances/bad-dead-control.tsx +6 -0
- package/corpus/affordances/good-live-control.tsx +12 -0
- package/corpus/api/store/history/find-orders.ts +8 -0
- package/corpus/api/store/history/route.ts +7 -0
- package/corpus/api/store/track/find-tracked-order.ts +12 -0
- package/corpus/api/store/track/route.ts +10 -0
- package/corpus/dates/bad-string-order.ts +7 -0
- package/corpus/dates/good-time-order.ts +7 -0
- package/corpus/failure/good-refresh.ts +17 -0
- package/corpus/failure/logged.ts +6 -0
- package/corpus/failure/refresh.ts +4 -0
- package/corpus/failure/reload.ts +6 -0
- package/corpus/manifest.json +6 -0
- package/corpus/medusa/queries/bad-unordered-page.ts +20 -0
- package/corpus/medusa/queries/good-ordered-page.ts +23 -0
- package/corpus/messaging/providers/email-gmail.ts +2 -0
- package/corpus/messaging/providers/sms-twilio.ts +2 -0
- package/corpus/messaging/providers/whatsapp-360dialog.ts +2 -0
- package/corpus/messaging/registry.ts +5 -0
- package/corpus/packages/pricing/package.json +5 -0
- package/corpus/packages/pricing/src/index.ts +2 -0
- package/corpus/packages/utils/package.json +5 -0
- package/corpus/packages/utils/src/index.ts +4 -0
- package/corpus/suites/__tests__/awaits.ts +17 -0
- package/corpus/suites/__tests__/settles.ts +14 -0
- package/dist/{chunk-LTDP2733.js → chunk-KV7JRNAP.js} +7 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +784 -52
- package/dist/presets.d.ts +3 -0
- package/dist/presets.js +1 -1
- package/package.json +2 -2
- /package/corpus/api/store/orders/{scoped.route.ts → scoped/route.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
PRESUMED_ENGINES,
|
|
4
4
|
RENAMED_TO,
|
|
5
5
|
rulesOfPreset
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-KV7JRNAP.js";
|
|
7
7
|
|
|
8
8
|
// src/index.ts
|
|
9
9
|
import { createRequire } from "module";
|
|
@@ -30,6 +30,11 @@ var isDoor = (filename, door) => {
|
|
|
30
30
|
return false;
|
|
31
31
|
}
|
|
32
32
|
};
|
|
33
|
+
var isModule = (filename, module) => {
|
|
34
|
+
const path2 = normalise(filename);
|
|
35
|
+
const named = module.replace(/^\.?\//, "");
|
|
36
|
+
return path2 === named || path2.endsWith(`/${named}`);
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
// src/rules/lib/imports.ts
|
|
35
40
|
var REQUIRE = "require";
|
|
@@ -1429,9 +1434,10 @@ var variableOf = (value) => /^var\(\s*(--[A-Za-z0-9_-]+)/.exec(value.trim())?.[1
|
|
|
1429
1434
|
var WEIGHT = /^\d+(?:\.\d+)?$/;
|
|
1430
1435
|
var fontRolesOnly = {
|
|
1431
1436
|
create(context) {
|
|
1432
|
-
const { allow = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
|
|
1437
|
+
const { allow = [], exemptModules = [], prefix = "--font-", roles } = context.options?.[0] ?? {};
|
|
1433
1438
|
const declared = requireOption("font-roles-only", roles, "roles");
|
|
1434
1439
|
const filename = normalise(context.filename);
|
|
1440
|
+
if (exemptModules.some((module) => isModule(filename, module))) return {};
|
|
1435
1441
|
if (allow.some((path2) => filename.includes(path2))) return {};
|
|
1436
1442
|
return {
|
|
1437
1443
|
JSXOpeningElement(node) {
|
|
@@ -1462,11 +1468,19 @@ var fontRolesOnly = {
|
|
|
1462
1468
|
sans-serif"\` or \`font-[Helvetica]\` with the role variable \u2014 \`var(--font-body)\`, \`var(--font-display)\`
|
|
1463
1469
|
\u2014 or the utility that reads it. A \`var(--font-\u2026)\` naming a role the contract does not declare is the
|
|
1464
1470
|
same offence: add the role to the theme, or use one that is there. Declare your role names in
|
|
1465
|
-
\`roles\` and, if your variables are spelled differently, set \`prefix\`;
|
|
1466
|
-
\`@font-face\` sheet in \`
|
|
1471
|
+
\`roles\` and, if your variables are spelled differently, set \`prefix\`; NAME the theme file and any
|
|
1472
|
+
\`@font-face\` sheet in \`exemptModules\`, one module per entry \u2014 \`allow\` still takes patterns and still
|
|
1473
|
+
grants its exemption to wherever the pattern stops, which is a boundary nobody decided and nobody
|
|
1474
|
+
counts. Bare \`font-*\` utility classes are deliberately not policed, because
|
|
1467
1475
|
Tailwind puts weight and style under the same prefix. Enabled with no \`roles\` the rule refuses the
|
|
1468
1476
|
run.`,
|
|
1469
1477
|
meta: {
|
|
1478
|
+
/**
|
|
1479
|
+
* #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
|
|
1480
|
+
* doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
|
|
1481
|
+
* grant nothing counts, which is the defect this pair exists to end.
|
|
1482
|
+
*/
|
|
1483
|
+
grants: { modules: "exemptModules", paths: "allow" },
|
|
1470
1484
|
docs: {
|
|
1471
1485
|
description: "Font families are asked for by role. A family literal in a className or an inline style fires, and so does a var() naming a role outside the declared set. Configure `roles` (and `prefix` when the variables are spelled differently); enabled without `roles` it refuses the run."
|
|
1472
1486
|
},
|
|
@@ -1479,6 +1493,7 @@ run.`,
|
|
|
1479
1493
|
additionalProperties: false,
|
|
1480
1494
|
properties: {
|
|
1481
1495
|
allow: { items: { type: "string" }, type: "array" },
|
|
1496
|
+
exemptModules: { items: { type: "string" }, type: "array" },
|
|
1482
1497
|
prefix: { type: "string" },
|
|
1483
1498
|
roles: { items: { type: "string" }, type: "array" }
|
|
1484
1499
|
},
|
|
@@ -1647,6 +1662,81 @@ export const geonosisProbeWall = geonosisProbe
|
|
|
1647
1662
|
};
|
|
1648
1663
|
var layer_walls_default = layerWalls;
|
|
1649
1664
|
|
|
1665
|
+
// src/rules/list-take-without-order.ts
|
|
1666
|
+
var CALLEES = [String.raw`^(?:\w+\.)?list[A-Z]`, String.raw`^(?:\w+\.)?graph$`];
|
|
1667
|
+
var ORDER_KEYS = ["order", "orderBy"];
|
|
1668
|
+
var TAKE = "take";
|
|
1669
|
+
var keyNameOf = (node) => {
|
|
1670
|
+
const key = node.key;
|
|
1671
|
+
if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
|
|
1672
|
+
return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
|
|
1673
|
+
};
|
|
1674
|
+
var spellingOf = (callee) => {
|
|
1675
|
+
if (callee === void 0) return null;
|
|
1676
|
+
if (callee.type === "Identifier") return callee.name ?? null;
|
|
1677
|
+
const property = propertyNameOf(callee);
|
|
1678
|
+
if (property === null) return null;
|
|
1679
|
+
const object = callee.object;
|
|
1680
|
+
return object?.type === "Identifier" ? `${String(object.name)}.${property}` : property;
|
|
1681
|
+
};
|
|
1682
|
+
var listTakeWithoutOrder = {
|
|
1683
|
+
create(context) {
|
|
1684
|
+
const options = bagOf(context.options);
|
|
1685
|
+
const callees = (options.callees ?? CALLEES).map((pattern) => new RegExp(pattern));
|
|
1686
|
+
const orderKeys = options.orderKeys ?? ORDER_KEYS;
|
|
1687
|
+
return {
|
|
1688
|
+
CallExpression(node) {
|
|
1689
|
+
const callee = spellingOf(node.callee);
|
|
1690
|
+
if (callee === null || !callees.some((pattern) => pattern.test(callee))) return;
|
|
1691
|
+
let takes = false;
|
|
1692
|
+
let orders = false;
|
|
1693
|
+
let unreadable = false;
|
|
1694
|
+
for (const argument of node.arguments ?? []) {
|
|
1695
|
+
walkNode(argument, (inner) => {
|
|
1696
|
+
if (inner.type === "SpreadElement") unreadable = true;
|
|
1697
|
+
if (inner.type !== "Property") return;
|
|
1698
|
+
const key = keyNameOf(inner);
|
|
1699
|
+
if (key === TAKE) takes = true;
|
|
1700
|
+
if (key !== null && orderKeys.includes(key)) orders = true;
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
if (!takes || orders || unreadable) return;
|
|
1704
|
+
context.report({
|
|
1705
|
+
data: { callee, order: orderKeys[0] ?? "" },
|
|
1706
|
+
messageId: "unordered",
|
|
1707
|
+
node
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
},
|
|
1712
|
+
fixShape: `A page taken in no order answers a question about the page, not about the table. A \`list*()\`
|
|
1713
|
+
or \`query.graph\` call carrying \`take\` names an \`order\` beside it \u2014 the column the page is a page OF \u2014
|
|
1714
|
+
so the first row is the first row every time, and the row a previous stage just wrote is where the
|
|
1715
|
+
reader expects it. Without one the database returns whatever is cheapest, which is stable in
|
|
1716
|
+
development and different under load. A spread the rule cannot read is left alone. Which calls are
|
|
1717
|
+
list calls is the \`callees\` option and which key declares the ordering is \`orderKeys\`.`,
|
|
1718
|
+
meta: {
|
|
1719
|
+
docs: {
|
|
1720
|
+
description: "A list or graph call that takes a page without declaring an order. The rows come back in whatever order is cheapest, so `[0]`, `.find()` and `.sort()` over the result answer a question about the page rather than about the table."
|
|
1721
|
+
},
|
|
1722
|
+
messages: {
|
|
1723
|
+
unordered: "`{{callee}}` takes a page with no `{{order}}`: the rows come back in whatever order the database finds cheapest, so the first row is a different row under load. Name the column the page is a page of."
|
|
1724
|
+
},
|
|
1725
|
+
schema: [
|
|
1726
|
+
{
|
|
1727
|
+
additionalProperties: false,
|
|
1728
|
+
properties: {
|
|
1729
|
+
callees: { items: { type: "string" }, type: "array" },
|
|
1730
|
+
orderKeys: { items: { type: "string" }, type: "array" }
|
|
1731
|
+
},
|
|
1732
|
+
type: "object"
|
|
1733
|
+
}
|
|
1734
|
+
],
|
|
1735
|
+
type: "problem"
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
var list_take_without_order_default = listTakeWithoutOrder;
|
|
1739
|
+
|
|
1650
1740
|
// src/rules/max-comment-density.ts
|
|
1651
1741
|
var exemptFiles2 = [
|
|
1652
1742
|
/\/__tests__\//,
|
|
@@ -2428,6 +2518,79 @@ saga, not the transaction.`,
|
|
|
2428
2518
|
};
|
|
2429
2519
|
var no_d1_transaction_default = noD1Transaction;
|
|
2430
2520
|
|
|
2521
|
+
// src/rules/no-date-string-compare.ts
|
|
2522
|
+
var FIELD_PATTERN = "(?:_at|[a-z]At)$";
|
|
2523
|
+
var RELATIONAL = /* @__PURE__ */ new Set(["<", "<=", ">", ">="]);
|
|
2524
|
+
var COMPARE = "localeCompare";
|
|
2525
|
+
var STRINGIFY = "String";
|
|
2526
|
+
var fieldStringified = (node, holdsATime) => {
|
|
2527
|
+
if (node === void 0) return null;
|
|
2528
|
+
const named = (subject) => {
|
|
2529
|
+
if (subject === void 0) return null;
|
|
2530
|
+
const property = propertyNameOf(subject);
|
|
2531
|
+
if (property !== null) return holdsATime.test(property) ? property : null;
|
|
2532
|
+
return subject.type === "Identifier" && typeof subject.name === "string" && holdsATime.test(subject.name) ? subject.name : null;
|
|
2533
|
+
};
|
|
2534
|
+
if (node.type === "CallExpression" && calleeNameOf(node) === STRINGIFY) {
|
|
2535
|
+
const [argument] = node.arguments ?? [];
|
|
2536
|
+
return named(argument);
|
|
2537
|
+
}
|
|
2538
|
+
if (node.type !== "TemplateLiteral") return null;
|
|
2539
|
+
const expressions = node.expressions ?? [];
|
|
2540
|
+
const quasis = node.quasis ?? [];
|
|
2541
|
+
const empty = quasis.every((one) => (one.value?.raw ?? "") === "");
|
|
2542
|
+
return expressions.length === 1 && empty ? named(expressions[0]) : null;
|
|
2543
|
+
};
|
|
2544
|
+
var noDateStringCompare = {
|
|
2545
|
+
create(context) {
|
|
2546
|
+
const holdsATime = new RegExp(bagOf(context.options).fieldPattern ?? FIELD_PATTERN);
|
|
2547
|
+
const reportFirst = (node, sides, ordering) => {
|
|
2548
|
+
for (const side of sides) {
|
|
2549
|
+
const field = fieldStringified(side, holdsATime);
|
|
2550
|
+
if (field === null) continue;
|
|
2551
|
+
context.report({ data: { field, ordering }, messageId: "textOrder", node });
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
return {
|
|
2556
|
+
BinaryExpression(node) {
|
|
2557
|
+
if (!RELATIONAL.has(node.operator)) return;
|
|
2558
|
+
reportFirst(node, [node.left, node.right], node.operator);
|
|
2559
|
+
},
|
|
2560
|
+
CallExpression(node) {
|
|
2561
|
+
if (propertyNameOf(node.callee) !== COMPARE) return;
|
|
2562
|
+
const receiver = node.callee.object;
|
|
2563
|
+
const [argument] = node.arguments ?? [];
|
|
2564
|
+
reportFirst(node, [receiver, argument], COMPARE);
|
|
2565
|
+
}
|
|
2566
|
+
};
|
|
2567
|
+
},
|
|
2568
|
+
fixShape: `Order a timestamp by its time, never by its text. \`String(row.created_at)\` on a Date is
|
|
2569
|
+
"Fri Aug 30 2026 \u2026", so "newest first" becomes "starts with F" and the sort is stable, plausible and
|
|
2570
|
+
wrong. Compare \`new Date(a.created_at).getTime()\` against \`Date.parse(b.created_at)\`, or sort on the
|
|
2571
|
+
ISO string the API returned \u2014 an ISO-8601 string does order lexicographically, which is why the
|
|
2572
|
+
defect survives review. The rule reports only a field turned INTO text: a raw timestamp under \`<\`
|
|
2573
|
+
and a template with copy in it are both left alone. Which names hold a timestamp is the
|
|
2574
|
+
\`fieldPattern\` option, defaulting to the \`_at\` / \`\u2026At\` spellings.`,
|
|
2575
|
+
meta: {
|
|
2576
|
+
docs: {
|
|
2577
|
+
description: "A `*_at` / `*At` field turned into a string with `String()` or a bare template, then ordered with `localeCompare` or a relational operator. A stringified Date sorts by its weekday name, not by its time."
|
|
2578
|
+
},
|
|
2579
|
+
messages: {
|
|
2580
|
+
textOrder: 'Ordering "{{field}}" by {{ordering}} on a string compares the TEXT of a timestamp: a Date stringifies to "Fri Aug 30 2026 \u2026", so this sorts by weekday name. Compare `.getTime()` or `Date.parse()`, or order on the ISO string the API returned.'
|
|
2581
|
+
},
|
|
2582
|
+
schema: [
|
|
2583
|
+
{
|
|
2584
|
+
additionalProperties: false,
|
|
2585
|
+
properties: { fieldPattern: { type: "string" } },
|
|
2586
|
+
type: "object"
|
|
2587
|
+
}
|
|
2588
|
+
],
|
|
2589
|
+
type: "problem"
|
|
2590
|
+
}
|
|
2591
|
+
};
|
|
2592
|
+
var no_date_string_compare_default = noDateStringCompare;
|
|
2593
|
+
|
|
2431
2594
|
// src/rules/no-duplicate-jsx-patterns.ts
|
|
2432
2595
|
var COMPONENT_FILE_PATTERN = /(?:features\/[^/]+\/)?(compounds|organelles|cells|tissues|organs)\/.+\.tsx$/;
|
|
2433
2596
|
var MIN_CLASS_LENGTH = 30;
|
|
@@ -2520,6 +2683,90 @@ their text are one component with a prop.`,
|
|
|
2520
2683
|
};
|
|
2521
2684
|
var no_duplicate_jsx_patterns_default = noDuplicateJsxPatterns;
|
|
2522
2685
|
|
|
2686
|
+
// src/rules/no-generic-utility-package.ts
|
|
2687
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2688
|
+
var NAMES = ["common", "helpers", "misc", "shared", "utils"];
|
|
2689
|
+
var ENTRIES = [
|
|
2690
|
+
"index.ts",
|
|
2691
|
+
"index.tsx",
|
|
2692
|
+
"index.js",
|
|
2693
|
+
"index.jsx",
|
|
2694
|
+
"src/index.ts",
|
|
2695
|
+
"src/index.tsx",
|
|
2696
|
+
"src/index.js",
|
|
2697
|
+
"src/index.jsx"
|
|
2698
|
+
];
|
|
2699
|
+
var MANIFEST = "package.json";
|
|
2700
|
+
var nameOf2 = (root) => {
|
|
2701
|
+
try {
|
|
2702
|
+
const manifest = JSON.parse(readFileSync3(`${root}/${MANIFEST}`, "utf8"));
|
|
2703
|
+
return typeof manifest.name === "string" ? manifest.name : null;
|
|
2704
|
+
} catch {
|
|
2705
|
+
return null;
|
|
2706
|
+
}
|
|
2707
|
+
};
|
|
2708
|
+
var noGenericUtilityPackage = {
|
|
2709
|
+
create(context) {
|
|
2710
|
+
const options = bagOf(context.options);
|
|
2711
|
+
const names2 = new Set(options.names ?? NAMES);
|
|
2712
|
+
const entries = options.entries ?? ENTRIES;
|
|
2713
|
+
const filename = normalise(context.filename);
|
|
2714
|
+
return {
|
|
2715
|
+
Program(node) {
|
|
2716
|
+
const entry = entries.toSorted((a, b) => b.length - a.length).find((one) => filename.endsWith(`/${one}`));
|
|
2717
|
+
if (entry === void 0) return;
|
|
2718
|
+
const name = nameOf2(filename.slice(0, filename.length - entry.length - 1));
|
|
2719
|
+
if (name === null) return;
|
|
2720
|
+
const last = name.split("/").at(-1) ?? "";
|
|
2721
|
+
if (!names2.has(last)) return;
|
|
2722
|
+
context.report({ data: { generic: last, name }, messageId: "generic", node });
|
|
2723
|
+
}
|
|
2724
|
+
};
|
|
2725
|
+
},
|
|
2726
|
+
fixShape: `A package is named for what it does. \`utils\`, \`common\`, \`helpers\`, \`shared\`, \`misc\` say nothing
|
|
2727
|
+
about what is inside, so nothing can be refused entry and everything arrives: web helpers, a constants
|
|
2728
|
+
object, a time door. Split it by what each export is FOR and give each half a name that would make a
|
|
2729
|
+
wrong import obviously wrong \u2014 the formatting goes to the feature's own lib, the constants go beside
|
|
2730
|
+
their owner, a door gets its own package named after the thing it is a door to. Delete the rest. The
|
|
2731
|
+
anti-names are the \`names\` option and the default is the universal list, never a repo's vocabulary.`,
|
|
2732
|
+
meta: {
|
|
2733
|
+
docs: {
|
|
2734
|
+
description: "A workspace package named `utils` / `common` / `helpers` / `shared` / `misc` is named for nothing, so it accretes everything. Reported once, at the package entry, from the name in its own manifest."
|
|
2735
|
+
},
|
|
2736
|
+
messages: {
|
|
2737
|
+
generic: 'The package "{{name}}" is named for nothing: "{{generic}}" says what is inside to no one, so nothing can be refused entry and everything arrives. Split it by what each export is for and name each half after the thing it does.'
|
|
2738
|
+
},
|
|
2739
|
+
schema: [
|
|
2740
|
+
{
|
|
2741
|
+
additionalProperties: false,
|
|
2742
|
+
properties: {
|
|
2743
|
+
entries: { items: { type: "string" }, type: "array" },
|
|
2744
|
+
names: { items: { type: "string" }, type: "array" }
|
|
2745
|
+
},
|
|
2746
|
+
type: "object"
|
|
2747
|
+
}
|
|
2748
|
+
],
|
|
2749
|
+
type: "problem"
|
|
2750
|
+
},
|
|
2751
|
+
/** The manifest AND the entry: the name is in the manifest, and the finding lands on the entry. */
|
|
2752
|
+
probe(options) {
|
|
2753
|
+
const bag = bagOf(options);
|
|
2754
|
+
const generic = (bag.names ?? NAMES)[0] ?? "";
|
|
2755
|
+
const entry = (bag.entries ?? ENTRIES)[0] ?? "";
|
|
2756
|
+
return {
|
|
2757
|
+
files: [
|
|
2758
|
+
{ path: `packages/${generic}/${entry}`, source: "export const entry = 1\n" },
|
|
2759
|
+
{
|
|
2760
|
+
path: `packages/${generic}/${MANIFEST}`,
|
|
2761
|
+
source: `{ "name": "${generic}", "version": "0.0.0" }
|
|
2762
|
+
`
|
|
2763
|
+
}
|
|
2764
|
+
]
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
};
|
|
2768
|
+
var no_generic_utility_package_default = noGenericUtilityPackage;
|
|
2769
|
+
|
|
2523
2770
|
// src/rules/no-hook-in-component-disguise.ts
|
|
2524
2771
|
var EXPLICIT_SIDE_EFFECT_HOOKS = /* @__PURE__ */ new Set([
|
|
2525
2772
|
"useEffect",
|
|
@@ -3567,6 +3814,154 @@ The state goes in an organelle that renders its own JSX; the arrangement goes in
|
|
|
3567
3814
|
};
|
|
3568
3815
|
var no_render_prop_reader_default = noRenderPropReader;
|
|
3569
3816
|
|
|
3817
|
+
// src/rules/no-runtime-dead-affordance.ts
|
|
3818
|
+
var HANDS_IT_ON = /* @__PURE__ */ new Set(["asChild", "disabled", "form", "formAction"]);
|
|
3819
|
+
var PRESSED_BY_THE_FORM = /* @__PURE__ */ new Set(["submit", "reset"]);
|
|
3820
|
+
var nameOf3 = (attribute) => attribute.name?.type === "JSXIdentifier" ? attribute.name.name ?? "" : "";
|
|
3821
|
+
var arms = (attribute) => {
|
|
3822
|
+
if (attribute.type !== "JSXAttribute") return true;
|
|
3823
|
+
const name = nameOf3(attribute);
|
|
3824
|
+
if (name.startsWith("on") && name.length > 2) return true;
|
|
3825
|
+
if (HANDS_IT_ON.has(name)) return true;
|
|
3826
|
+
if (name !== "type") return false;
|
|
3827
|
+
const value = attribute.value;
|
|
3828
|
+
if (value === null || value === void 0) return false;
|
|
3829
|
+
return value.type === "Literal" ? PRESSED_BY_THE_FORM.has(String(value.value)) : (
|
|
3830
|
+
// An expression is a type this rule cannot read.
|
|
3831
|
+
true
|
|
3832
|
+
);
|
|
3833
|
+
};
|
|
3834
|
+
var noRuntimeDeadAffordance = {
|
|
3835
|
+
create(context) {
|
|
3836
|
+
const controls = new Set((context.options[0] ?? []).map((one) => one.atom));
|
|
3837
|
+
return {
|
|
3838
|
+
JSXOpeningElement(node) {
|
|
3839
|
+
if (node?.name?.type !== "JSXIdentifier") return;
|
|
3840
|
+
const atom = node.name.name ?? "";
|
|
3841
|
+
if (!controls.has(atom)) return;
|
|
3842
|
+
if ((node.attributes ?? []).some(arms)) return;
|
|
3843
|
+
context.report({ data: { atom }, messageId: "dead", node });
|
|
3844
|
+
}
|
|
3845
|
+
};
|
|
3846
|
+
},
|
|
3847
|
+
fixShape: `A control that renders and does nothing is worse than no control: it looks pressable and
|
|
3848
|
+
answers nothing. Every control atom the rule's map names carries one of \`onClick\` (or another
|
|
3849
|
+
\`on\u2026\` handler), \`asChild\` so the child it wraps carries the behaviour, \`type="submit"\`/\`"reset"\` so
|
|
3850
|
+
the form presses it, \`form\`/\`formAction\`, or \`disabled\` when it is inert on purpose. \`type="button"\`
|
|
3851
|
+
is the default and presses nothing, so it arms nothing. A spread is a prop bag the rule cannot read
|
|
3852
|
+
and is left alone. Configure the same element\u2192atom map \`no-raw-html-atoms\` takes; enabled without
|
|
3853
|
+
one the rule refuses the run.`,
|
|
3854
|
+
meta: {
|
|
3855
|
+
docs: {
|
|
3856
|
+
description: "A control atom rendered with no handler, no `asChild`, no submit role and no `disabled` is a dead affordance \u2014 it renders, it looks pressable, and pressing it does nothing. Reads the same element\u2192atom map as `no-raw-html-atoms`."
|
|
3857
|
+
},
|
|
3858
|
+
messages: {
|
|
3859
|
+
dead: '<{{atom}}> has no onClick, no other handler, no asChild and no submit role, so pressing it does nothing. Give it a handler, hand the behaviour to a child with asChild, make it type="submit" inside its form, or mark it disabled if it is meant to be inert.'
|
|
3860
|
+
},
|
|
3861
|
+
schema: [
|
|
3862
|
+
{
|
|
3863
|
+
items: {
|
|
3864
|
+
properties: {
|
|
3865
|
+
atom: { type: "string" },
|
|
3866
|
+
element: { type: "string" },
|
|
3867
|
+
importPath: { type: "string" }
|
|
3868
|
+
},
|
|
3869
|
+
required: ["element", "atom", "importPath"],
|
|
3870
|
+
type: "object"
|
|
3871
|
+
},
|
|
3872
|
+
type: "array"
|
|
3873
|
+
}
|
|
3874
|
+
],
|
|
3875
|
+
type: "problem"
|
|
3876
|
+
},
|
|
3877
|
+
/** The map is POSITIONAL, so the probe reads `options[0]` rather than a bag. */
|
|
3878
|
+
probe(options) {
|
|
3879
|
+
const map = Array.isArray(options[0]) ? options[0] : [];
|
|
3880
|
+
const atom = map[0]?.atom;
|
|
3881
|
+
if (atom === void 0) {
|
|
3882
|
+
throw new Error(
|
|
3883
|
+
"biological-architecture/no-runtime-dead-affordance has no element\u2192atom map here, so there is no control for a probe to render \u2014 the rule is inert by design until one is configured."
|
|
3884
|
+
);
|
|
3885
|
+
}
|
|
3886
|
+
return {
|
|
3887
|
+
path: "cells/geonosis-probe.tsx",
|
|
3888
|
+
source: `export const GeonosisProbe = () => <${atom} />
|
|
3889
|
+
`
|
|
3890
|
+
};
|
|
3891
|
+
}
|
|
3892
|
+
};
|
|
3893
|
+
var no_runtime_dead_affordance_default = noRuntimeDeadAffordance;
|
|
3894
|
+
|
|
3895
|
+
// src/rules/no-swallowed-catch.ts
|
|
3896
|
+
var CATCH = "catch";
|
|
3897
|
+
var LOGGER = "console";
|
|
3898
|
+
var FUNCTIONS = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
|
3899
|
+
var isLogging = (node) => {
|
|
3900
|
+
const callee = node.callee;
|
|
3901
|
+
const object = callee?.object;
|
|
3902
|
+
return object?.type === "Identifier" && object.name === LOGGER;
|
|
3903
|
+
};
|
|
3904
|
+
var swallows = (body) => {
|
|
3905
|
+
let answers = false;
|
|
3906
|
+
walkNode(body, (node) => {
|
|
3907
|
+
if (node.type === "ThrowStatement") answers = true;
|
|
3908
|
+
if (node.type === "AssignmentExpression" || node.type === "UpdateExpression") answers = true;
|
|
3909
|
+
if (node.type === "CallExpression" && !isLogging(node)) answers = true;
|
|
3910
|
+
});
|
|
3911
|
+
return !answers;
|
|
3912
|
+
};
|
|
3913
|
+
var noSwallowedCatch = {
|
|
3914
|
+
create(context) {
|
|
3915
|
+
const paths = bagOf(context.options).paths ?? [];
|
|
3916
|
+
const filename = normalise(context.filename);
|
|
3917
|
+
if (!paths.some((pattern) => new RegExp(pattern).test(filename))) return {};
|
|
3918
|
+
const report = (node, shape) => {
|
|
3919
|
+
context.report({ data: { shape }, messageId: "swallowed", node });
|
|
3920
|
+
};
|
|
3921
|
+
return {
|
|
3922
|
+
CallExpression(node) {
|
|
3923
|
+
if (propertyNameOf(node.callee) !== CATCH) return;
|
|
3924
|
+
const [handler] = node.arguments ?? [];
|
|
3925
|
+
if (handler === void 0 || !FUNCTIONS.has(handler.type ?? "")) return;
|
|
3926
|
+
if (swallows(handler.body)) report(handler, ".catch()");
|
|
3927
|
+
},
|
|
3928
|
+
CatchClause(node) {
|
|
3929
|
+
if (swallows(node.body)) report(node, "catch block");
|
|
3930
|
+
}
|
|
3931
|
+
};
|
|
3932
|
+
},
|
|
3933
|
+
fixShape: `A rejection on a fetch or refresh path is answered, never dropped. An empty \`.catch(() => {})\`,
|
|
3934
|
+
an empty \`catch {}\` and a handler that only logs all leave the caller reading a failed refresh as a
|
|
3935
|
+
successful one. Write the failure somewhere the caller can see it \u2014 set an error flag, put the state
|
|
3936
|
+
back, mark the value stale \u2014 or rethrow it and let the boundary above decide. Which modules are the
|
|
3937
|
+
fetch/refresh surface is the \`paths\` option; enabled without it the rule refuses the run, because a
|
|
3938
|
+
rule with no surface to watch reads exactly like a tree with no swallowed rejection in it.`,
|
|
3939
|
+
meta: {
|
|
3940
|
+
docs: {
|
|
3941
|
+
description: "A `.catch()` handler or a `catch` block on a declared fetch/refresh path must write state or rethrow. An empty handler, or one that only logs, turns a failed refresh into a successful one for everything downstream."
|
|
3942
|
+
},
|
|
3943
|
+
messages: {
|
|
3944
|
+
swallowed: "This {{shape}} drops the rejection: it neither writes state nor rethrows, so a failed refresh is indistinguishable from a successful one. Set the error where the caller reads it, or rethrow."
|
|
3945
|
+
},
|
|
3946
|
+
schema: [
|
|
3947
|
+
{
|
|
3948
|
+
additionalProperties: false,
|
|
3949
|
+
properties: { paths: { items: { type: "string" }, type: "array" } },
|
|
3950
|
+
type: "object"
|
|
3951
|
+
}
|
|
3952
|
+
],
|
|
3953
|
+
type: "problem"
|
|
3954
|
+
},
|
|
3955
|
+
probe(options) {
|
|
3956
|
+
return {
|
|
3957
|
+
path: pathMatching(firstOf(bagOf(options).paths, []), "refresh.ts"),
|
|
3958
|
+
source: `export const refresh = (load: () => Promise<void>) => load().catch(() => {})
|
|
3959
|
+
`
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
};
|
|
3963
|
+
var no_swallowed_catch_default = noSwallowedCatch;
|
|
3964
|
+
|
|
3570
3965
|
// src/rules/no-trivial-wrapper-component.ts
|
|
3571
3966
|
var ROUTE_BASENAMES = /* @__PURE__ */ new Set([
|
|
3572
3967
|
"default.tsx",
|
|
@@ -3802,9 +4197,9 @@ and is imported.`,
|
|
|
3802
4197
|
var no_type_definitions_in_components_default = noTypeDefinitionsInComponents;
|
|
3803
4198
|
|
|
3804
4199
|
// src/rules/no-unregistered-plugin-dir.ts
|
|
3805
|
-
import { readFileSync as
|
|
4200
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3806
4201
|
var RULE11 = "no-unregistered-plugin-dir";
|
|
3807
|
-
var ACCEPTS3 = "{ roots: string[], registry: string | string[], manifests: string[] }";
|
|
4202
|
+
var ACCEPTS3 = "{ roots: (string | { path: string, files: boolean })[], registry: string | string[], manifests: string[] }";
|
|
3808
4203
|
var trimSlashes = (path2) => path2.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
3809
4204
|
var withoutComments = (source) => source.replaceAll(/\/\*[\s\S]*?\*\//g, " ").replaceAll(/\/\/[^\n]*/g, " ");
|
|
3810
4205
|
var literalsOf = (source) => [...withoutComments(source).matchAll(/['"`]([^'"`\n]+)['"`]/g)].map((match) => match[1] ?? "");
|
|
@@ -3818,10 +4213,22 @@ var rootedAt = (filename, root) => {
|
|
|
3818
4213
|
if (at < 0) return null;
|
|
3819
4214
|
return { after: filename.slice(at + marker.length), repoRoot: filename.slice(0, at) };
|
|
3820
4215
|
};
|
|
4216
|
+
var oneDirectory = (segments, manifests) => {
|
|
4217
|
+
if (segments.length !== 2) return null;
|
|
4218
|
+
const [directory, file] = segments;
|
|
4219
|
+
return directory !== void 0 && file !== void 0 && manifests.has(file) ? directory : null;
|
|
4220
|
+
};
|
|
4221
|
+
var oneFile = (segments) => {
|
|
4222
|
+
if (segments.length !== 1) return null;
|
|
4223
|
+
const [file] = segments;
|
|
4224
|
+
return file === void 0 ? null : file.replace(/\.[cm]?[jt]sx?$/, "");
|
|
4225
|
+
};
|
|
3821
4226
|
var noUnregisteredPluginDir = {
|
|
3822
4227
|
create(context) {
|
|
3823
4228
|
const options = context.options?.[0] ?? {};
|
|
3824
|
-
const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(
|
|
4229
|
+
const roots = requireOption(RULE11, options.roots, "roots", ACCEPTS3).map(
|
|
4230
|
+
(one) => typeof one === "string" ? { files: false, path: trimSlashes(one) } : { files: one.files === true, path: trimSlashes(one.path) }
|
|
4231
|
+
);
|
|
3825
4232
|
const declared = requireOption(RULE11, options.registry, "registry", ACCEPTS3);
|
|
3826
4233
|
const registries = (typeof declared === "string" ? [declared] : declared).map(trimSlashes);
|
|
3827
4234
|
const manifests = new Set(requireOption(RULE11, options.manifests, "manifests", ACCEPTS3));
|
|
@@ -3829,21 +4236,24 @@ var noUnregisteredPluginDir = {
|
|
|
3829
4236
|
return {
|
|
3830
4237
|
Program(node) {
|
|
3831
4238
|
for (const root of roots) {
|
|
3832
|
-
const rooted = rootedAt(filename, root);
|
|
4239
|
+
const rooted = rootedAt(filename, root.path);
|
|
3833
4240
|
if (rooted === null) continue;
|
|
3834
4241
|
const segments = rooted.after.split("/");
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
if (directory === void 0 || file === void 0 || !manifests.has(file)) continue;
|
|
4242
|
+
const registrant = root.files ? oneFile(segments) : oneDirectory(segments, manifests);
|
|
4243
|
+
if (registrant === null) continue;
|
|
3838
4244
|
let source = "";
|
|
3839
4245
|
try {
|
|
3840
|
-
source = registries.map((half) =>
|
|
4246
|
+
source = registries.map((half) => readFileSync4(`${rooted.repoRoot}/${half}`, "utf8")).join("\n");
|
|
3841
4247
|
} catch {
|
|
3842
4248
|
return;
|
|
3843
4249
|
}
|
|
3844
|
-
if (literalsOf(source).some((literal) => names(literal,
|
|
4250
|
+
if (literalsOf(source).some((literal) => names(literal, registrant))) return;
|
|
3845
4251
|
const registry = registries.join(", ");
|
|
3846
|
-
context.report({
|
|
4252
|
+
context.report({
|
|
4253
|
+
data: { directory: registrant, registry, root: root.path },
|
|
4254
|
+
messageId: "unregistered",
|
|
4255
|
+
node
|
|
4256
|
+
});
|
|
3847
4257
|
return;
|
|
3848
4258
|
}
|
|
3849
4259
|
}
|
|
@@ -3856,7 +4266,9 @@ eight of them. Configure \`roots\`, \`registry\` and \`manifests\` \u2014 all th
|
|
|
3856
4266
|
registry is SPLIT across files (a config that imports a second config): name every half, because the
|
|
3857
4267
|
rule reads source and does not follow imports. \`manifests\` has no default on purpose: dielime's plugin directories hold \`service.ts\` and no index, so a defaulted list
|
|
3858
4268
|
would match nothing and report a clean tree it never looked at. A seam whose providers are flat files
|
|
3859
|
-
rather than directories is
|
|
4269
|
+
rather than directories is judged when its root says \`files: true\` \u2014 \`{ path, files: true }\` makes each
|
|
4270
|
+
FILE under that root a registrant named by its own basename \u2014 and is out of scope under a bare root,
|
|
4271
|
+
where nothing matches and nothing is reported.`,
|
|
3860
4272
|
meta: {
|
|
3861
4273
|
docs: {
|
|
3862
4274
|
description: "Every directory under a plugin root must be named by the registry file. Midday ships 35 app-store directories and 8 are reachable from nothing. The registry is read as source, so an id or an import path both count, and `registry` takes a LIST when the registry is split across files \u2014 reading only the named half reported two registered carrier directories as dead weight on a real consumer. `roots`, `registry` and `manifests` are all required; `manifests` has no default because a defaulted entry filename matched nothing in a real consumer and reported a clean tree it had never looked at."
|
|
@@ -3875,7 +4287,23 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
|
|
|
3875
4287
|
{ items: { type: "string" }, type: "array" }
|
|
3876
4288
|
]
|
|
3877
4289
|
},
|
|
3878
|
-
roots: {
|
|
4290
|
+
roots: {
|
|
4291
|
+
items: {
|
|
4292
|
+
anyOf: [
|
|
4293
|
+
{ type: "string" },
|
|
4294
|
+
{
|
|
4295
|
+
additionalProperties: false,
|
|
4296
|
+
properties: {
|
|
4297
|
+
files: { type: "boolean" },
|
|
4298
|
+
path: { type: "string" }
|
|
4299
|
+
},
|
|
4300
|
+
required: ["path"],
|
|
4301
|
+
type: "object"
|
|
4302
|
+
}
|
|
4303
|
+
]
|
|
4304
|
+
},
|
|
4305
|
+
type: "array"
|
|
4306
|
+
}
|
|
3879
4307
|
},
|
|
3880
4308
|
type: "object"
|
|
3881
4309
|
}
|
|
@@ -3896,12 +4324,14 @@ rather than directories is out of scope \u2014 nothing under a root matches, so
|
|
|
3896
4324
|
*/
|
|
3897
4325
|
probe(options) {
|
|
3898
4326
|
const { manifests = [], registry = "", roots = [] } = bagOf(options);
|
|
3899
|
-
const
|
|
4327
|
+
const first = roots[0] ?? "";
|
|
4328
|
+
const root = typeof first === "string" ? { files: false, path: first } : first;
|
|
4329
|
+
const under = trimSlashes(root.path);
|
|
3900
4330
|
const half = typeof registry === "string" ? registry : registry[0] ?? "";
|
|
3901
4331
|
return {
|
|
3902
4332
|
files: [
|
|
3903
4333
|
{
|
|
3904
|
-
path: `${
|
|
4334
|
+
path: root.files === true ? `${under}/geonosis-probe.ts` : `${under}/geonosis-probe/${manifests[0] ?? ""}`,
|
|
3905
4335
|
source: "export const geonosisProbe = { id: 'geonosis-probe' }\n"
|
|
3906
4336
|
},
|
|
3907
4337
|
{ path: trimSlashes(half), source: "export const registry = { providers: [] }\n" }
|
|
@@ -4752,7 +5182,7 @@ var isFactShaped = (literal) => {
|
|
|
4752
5182
|
if (/^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/.test(literal)) return true;
|
|
4753
5183
|
return literal.length >= 16 && !/^[a-z0-9:\-[\]/.%#!]+$/.test(literal);
|
|
4754
5184
|
};
|
|
4755
|
-
var
|
|
5185
|
+
var nameOf4 = (node) => {
|
|
4756
5186
|
if (node === void 0) return void 0;
|
|
4757
5187
|
if (typeof node === "string") return node;
|
|
4758
5188
|
if (node.type === "Identifier" || node.type === "JSXIdentifier") return node.name;
|
|
@@ -4763,12 +5193,12 @@ var isSpelledAsStyleOrModule = (node) => {
|
|
|
4763
5193
|
let current = node.parent;
|
|
4764
5194
|
let child = node;
|
|
4765
5195
|
while (current && typeof current.type === "string" && current.type !== "Program") {
|
|
4766
|
-
if (current.type === "JSXAttribute") return styleAttributes.has(
|
|
5196
|
+
if (current.type === "JSXAttribute") return styleAttributes.has(nameOf4(current.name) ?? "");
|
|
4767
5197
|
if (current.type === "Property" && current.key === child) return true;
|
|
4768
|
-
if (current.type === "Property" && styleAttributes.has(
|
|
5198
|
+
if (current.type === "Property" && styleAttributes.has(nameOf4(current.key) ?? "")) return true;
|
|
4769
5199
|
if (current.type === "CallExpression") {
|
|
4770
5200
|
const callee = current.callee;
|
|
4771
|
-
const calleeName = callee?.type === "MemberExpression" ?
|
|
5201
|
+
const calleeName = callee?.type === "MemberExpression" ? nameOf4(callee.property) : nameOf4(callee);
|
|
4772
5202
|
if (styleCallees.has(calleeName ?? "") || formFieldCallees.has(calleeName ?? "")) return true;
|
|
4773
5203
|
const firstArgument = current.arguments?.[0];
|
|
4774
5204
|
if (declarationCallees.has(calleeName ?? "") && firstArgument === child) return true;
|
|
@@ -5060,8 +5490,8 @@ export const probe = step('geonosis-probe', async () => ({ ok: true }))
|
|
|
5060
5490
|
var step_opens_its_own_cell_default = stepOpensItsOwnCell;
|
|
5061
5491
|
|
|
5062
5492
|
// src/rules/store-route-scopes-tenant-data.ts
|
|
5063
|
-
|
|
5064
|
-
var
|
|
5493
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
5494
|
+
var ENTITIES = [
|
|
5065
5495
|
"cart",
|
|
5066
5496
|
"claim",
|
|
5067
5497
|
"company",
|
|
@@ -5071,27 +5501,136 @@ var TENANT_ENTITIES = /* @__PURE__ */ new Set([
|
|
|
5071
5501
|
"planned_order",
|
|
5072
5502
|
"production_order",
|
|
5073
5503
|
"quote"
|
|
5504
|
+
];
|
|
5505
|
+
var ROUTES = [String.raw`/api/store/.*/route\.tsx?$`];
|
|
5506
|
+
var SCOPING_CALLS = ["auth_context", "actor_id", "customer_id", "company_id"];
|
|
5507
|
+
var ENTITY_KEY = "entity";
|
|
5508
|
+
var SUFFIXES = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx", "/index.js"];
|
|
5509
|
+
var keyNameOf2 = (node) => {
|
|
5510
|
+
const key = node.key;
|
|
5511
|
+
if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
|
|
5512
|
+
return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
|
|
5513
|
+
};
|
|
5514
|
+
var stringValueOf = (node) => {
|
|
5515
|
+
const value = node.value;
|
|
5516
|
+
return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
|
|
5517
|
+
};
|
|
5518
|
+
var withoutComments2 = (text) => {
|
|
5519
|
+
let out = "";
|
|
5520
|
+
let at = 0;
|
|
5521
|
+
let quote = "";
|
|
5522
|
+
while (at < text.length) {
|
|
5523
|
+
const here = text[at] ?? "";
|
|
5524
|
+
const next = text[at + 1] ?? "";
|
|
5525
|
+
if (quote !== "") {
|
|
5526
|
+
if (here === "\\") {
|
|
5527
|
+
out += " ";
|
|
5528
|
+
at += 2;
|
|
5529
|
+
continue;
|
|
5530
|
+
}
|
|
5531
|
+
if (here === quote) quote = "";
|
|
5532
|
+
out += here;
|
|
5533
|
+
at += 1;
|
|
5534
|
+
continue;
|
|
5535
|
+
}
|
|
5536
|
+
if (here === '"' || here === "'" || here === "`") {
|
|
5537
|
+
quote = here;
|
|
5538
|
+
out += here;
|
|
5539
|
+
at += 1;
|
|
5540
|
+
continue;
|
|
5541
|
+
}
|
|
5542
|
+
if (here === "/" && next === "/") {
|
|
5543
|
+
while (at < text.length && text[at] !== "\n") {
|
|
5544
|
+
out += " ";
|
|
5545
|
+
at += 1;
|
|
5546
|
+
}
|
|
5547
|
+
continue;
|
|
5548
|
+
}
|
|
5549
|
+
if (here === "/" && next === "*") {
|
|
5550
|
+
const end = text.indexOf("*/", at + 2);
|
|
5551
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
5552
|
+
for (; at < stop; at += 1) out += text[at] === "\n" ? "\n" : " ";
|
|
5553
|
+
continue;
|
|
5554
|
+
}
|
|
5555
|
+
out += here;
|
|
5556
|
+
at += 1;
|
|
5557
|
+
}
|
|
5558
|
+
return out;
|
|
5559
|
+
};
|
|
5560
|
+
var siblingSourceOf = (importer, source) => {
|
|
5561
|
+
if (!source.startsWith(".")) return null;
|
|
5562
|
+
const landed = resolveRelative(importer, source);
|
|
5563
|
+
for (const suffix of SUFFIXES) {
|
|
5564
|
+
try {
|
|
5565
|
+
return withoutComments2(readFileSync5(`${landed}${suffix}`, "utf8"));
|
|
5566
|
+
} catch {
|
|
5567
|
+
continue;
|
|
5568
|
+
}
|
|
5569
|
+
}
|
|
5570
|
+
return null;
|
|
5571
|
+
};
|
|
5572
|
+
var MODULE_NODES = /* @__PURE__ */ new Set([
|
|
5573
|
+
"CallExpression",
|
|
5574
|
+
"ExportAllDeclaration",
|
|
5575
|
+
"ExportNamedDeclaration",
|
|
5576
|
+
"ImportDeclaration",
|
|
5577
|
+
"ImportExpression"
|
|
5074
5578
|
]);
|
|
5075
|
-
var
|
|
5579
|
+
var readIn = (text, entities) => {
|
|
5580
|
+
for (const entity of entities) {
|
|
5581
|
+
if (new RegExp(`${ENTITY_KEY}\\s*:\\s*['"\`]${entity}['"\`]`).test(text)) return entity;
|
|
5582
|
+
}
|
|
5583
|
+
return null;
|
|
5584
|
+
};
|
|
5585
|
+
var namesAny = (text, names2) => names2.some((name) => new RegExp(`\\b${name}\\b`).test(text));
|
|
5076
5586
|
var storeRouteScopesTenantData = {
|
|
5077
5587
|
create(context) {
|
|
5078
|
-
const
|
|
5079
|
-
|
|
5080
|
-
const
|
|
5588
|
+
const options = bagOf(context.options);
|
|
5589
|
+
const routes = options.routes ?? ROUTES;
|
|
5590
|
+
const entities = options.entities ?? ENTITIES;
|
|
5591
|
+
const narrowing = [...options.scopingCalls ?? SCOPING_CALLS, ...options.evidenceCalls ?? []];
|
|
5592
|
+
const filename = normalise(context.filename);
|
|
5593
|
+
if (!routes.some((pattern) => new RegExp(pattern).test(filename))) return {};
|
|
5594
|
+
const shortName = filename.split("/").slice(-3).join("/");
|
|
5081
5595
|
return {
|
|
5082
|
-
Program(
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
if (
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5596
|
+
Program(program) {
|
|
5597
|
+
let read = null;
|
|
5598
|
+
let scoped = false;
|
|
5599
|
+
const sources = [];
|
|
5600
|
+
walkNode(program, (node) => {
|
|
5601
|
+
if (node.type === "Property" && keyNameOf2(node) === ENTITY_KEY) {
|
|
5602
|
+
const entity = stringValueOf(node);
|
|
5603
|
+
if (entity !== null && entities.includes(entity) && read === null) {
|
|
5604
|
+
read = { entity, node };
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
if (node.type === "Identifier" && narrowing.includes(node.name)) scoped = true;
|
|
5608
|
+
if (MODULE_NODES.has(node.type ?? "")) {
|
|
5609
|
+
const source = sourceOf(node);
|
|
5610
|
+
if (source !== null) sources.push(source);
|
|
5094
5611
|
}
|
|
5612
|
+
});
|
|
5613
|
+
if (scoped) return;
|
|
5614
|
+
if (read !== null) {
|
|
5615
|
+
const found = read;
|
|
5616
|
+
context.report({
|
|
5617
|
+
data: { entity: found.entity, file: shortName },
|
|
5618
|
+
messageId: "unscoped",
|
|
5619
|
+
node: found.node
|
|
5620
|
+
});
|
|
5621
|
+
return;
|
|
5622
|
+
}
|
|
5623
|
+
for (const source of sources) {
|
|
5624
|
+
const text = siblingSourceOf(filename, source);
|
|
5625
|
+
if (text === null || namesAny(text, narrowing)) continue;
|
|
5626
|
+
const entity = readIn(text, entities);
|
|
5627
|
+
if (entity === null) continue;
|
|
5628
|
+
context.report({
|
|
5629
|
+
data: { entity, file: shortName, through: source },
|
|
5630
|
+
messageId: "unscopedSibling",
|
|
5631
|
+
node: program
|
|
5632
|
+
});
|
|
5633
|
+
return;
|
|
5095
5634
|
}
|
|
5096
5635
|
}
|
|
5097
5636
|
};
|
|
@@ -5099,17 +5638,43 @@ var storeRouteScopesTenantData = {
|
|
|
5099
5638
|
fixShape: `A customer-facing store route that reads a customer-owned entity (cart, order, quote, company, \u2026)
|
|
5100
5639
|
narrows the read to the authenticated caller \u2014 \`auth_context\`, \`actor_id\`, \`customer_id\` or
|
|
5101
5640
|
\`company_id\` must appear in the handler. Without it a publishable key returns every tenant's rows.
|
|
5102
|
-
|
|
5103
|
-
|
|
5641
|
+
A route the caller proves entitlement to with EVIDENCE instead of a session \u2014 an order reference
|
|
5642
|
+
plus the email it was placed with \u2014 calls a predicate the config names in \`evidenceCalls\`; the rule
|
|
5643
|
+
trusts no predicate it was not given. Moving the read into the module beside the route does not
|
|
5644
|
+
answer the question: the rule follows a relative import one hop and reads the scoping there. Which
|
|
5645
|
+
files are the route surface, which entities are customer-owned and which names count as scoping are
|
|
5646
|
+
all options. Global catalog entities are deliberately exempt; if the data is genuinely not customer
|
|
5647
|
+
data, it belongs on the admin surface.`,
|
|
5104
5648
|
meta: {
|
|
5105
5649
|
docs: {
|
|
5106
|
-
description: "A /store/** route that reads a customer-owned entity must narrow the read to the authenticated caller. Without
|
|
5650
|
+
description: "A /store/** route that reads a customer-owned entity must narrow the read to the authenticated caller, or verify caller-supplied evidence with a predicate the config names. Without either, the handler returns every tenant\u2019s rows to anyone holding a publishable key. The read is followed one hop into a relative sibling; global catalog entities are exempt."
|
|
5107
5651
|
},
|
|
5108
5652
|
messages: {
|
|
5109
|
-
unscoped: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" without any caller scoping (no auth_context / actor_id / customer_id / company_id in the file). Narrow the read to the authenticated caller, or move it to /admin if it is genuinely not customer data.'
|
|
5653
|
+
unscoped: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" without any caller scoping (no auth_context / actor_id / customer_id / company_id in the file, and no evidence check the config names). Narrow the read to the authenticated caller, name your evidence predicate in `evidenceCalls`, or move it to /admin if it is genuinely not customer data.',
|
|
5654
|
+
unscopedSibling: 'Store route "{{file}}" reads the customer-owned entity "{{entity}}" through "{{through}}", and neither file narrows it to a caller. Scope the read where it happens, or name the predicate that checks the caller\u2019s evidence in `evidenceCalls`.'
|
|
5110
5655
|
},
|
|
5111
|
-
schema: [
|
|
5656
|
+
schema: [
|
|
5657
|
+
{
|
|
5658
|
+
additionalProperties: false,
|
|
5659
|
+
properties: {
|
|
5660
|
+
entities: { items: { type: "string" }, type: "array" },
|
|
5661
|
+
evidenceCalls: { items: { type: "string" }, type: "array" },
|
|
5662
|
+
routes: { items: { type: "string" }, type: "array" },
|
|
5663
|
+
scopingCalls: { items: { type: "string" }, type: "array" }
|
|
5664
|
+
},
|
|
5665
|
+
type: "object"
|
|
5666
|
+
}
|
|
5667
|
+
],
|
|
5112
5668
|
type: "problem"
|
|
5669
|
+
},
|
|
5670
|
+
probe(options) {
|
|
5671
|
+
const bag = bagOf(options);
|
|
5672
|
+
return {
|
|
5673
|
+
path: pathMatching(firstOf(bag.routes, ROUTES), "route.ts"),
|
|
5674
|
+
source: `export const GET = async (query: { graph: (c: unknown) => Promise<unknown> }) =>
|
|
5675
|
+
query.graph({ entity: '${firstOf(bag.entities, ENTITIES)}' })
|
|
5676
|
+
`
|
|
5677
|
+
};
|
|
5113
5678
|
}
|
|
5114
5679
|
};
|
|
5115
5680
|
var store_route_scopes_tenant_data_default = storeRouteScopesTenantData;
|
|
@@ -5364,6 +5929,146 @@ table to \`schema/control/\`.`,
|
|
|
5364
5929
|
};
|
|
5365
5930
|
var tenant_tables_carry_org_id_default = tenantTablesCarryOrgId;
|
|
5366
5931
|
|
|
5932
|
+
// src/rules/test-no-clock-polling-for-promise-flips.ts
|
|
5933
|
+
var POLLERS = ["waitFor"];
|
|
5934
|
+
var TEST_GLOBS = [String.raw`\.(?:test|spec)\.[cm]?[jt]sx?$`, "/__tests__/"];
|
|
5935
|
+
var ASSERT = "expect";
|
|
5936
|
+
var FUNCTIONS2 = /* @__PURE__ */ new Set(["ArrowFunctionExpression", "FunctionDeclaration", "FunctionExpression"]);
|
|
5937
|
+
var walkWithin = (root, visit) => {
|
|
5938
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5939
|
+
const stack = [root];
|
|
5940
|
+
while (stack.length > 0) {
|
|
5941
|
+
const at = stack.pop();
|
|
5942
|
+
if (at === null || typeof at !== "object" || seen.has(at)) continue;
|
|
5943
|
+
seen.add(at);
|
|
5944
|
+
if (Array.isArray(at)) {
|
|
5945
|
+
stack.push(...at);
|
|
5946
|
+
continue;
|
|
5947
|
+
}
|
|
5948
|
+
const node = at;
|
|
5949
|
+
if (typeof node.type === "string") {
|
|
5950
|
+
if (at !== root && FUNCTIONS2.has(node.type)) continue;
|
|
5951
|
+
visit(node);
|
|
5952
|
+
}
|
|
5953
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5954
|
+
if (key === "parent") continue;
|
|
5955
|
+
if (value !== null && typeof value === "object") stack.push(value);
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
};
|
|
5959
|
+
var rootCallOf = (node) => {
|
|
5960
|
+
let at = node;
|
|
5961
|
+
for (; ; ) {
|
|
5962
|
+
const callee = at.callee;
|
|
5963
|
+
if (callee?.type !== "MemberExpression") return at;
|
|
5964
|
+
const object = callee.object;
|
|
5965
|
+
if (object?.type !== "CallExpression") return at;
|
|
5966
|
+
at = object;
|
|
5967
|
+
}
|
|
5968
|
+
};
|
|
5969
|
+
var onlyAsserts = (body) => {
|
|
5970
|
+
let calls = 0;
|
|
5971
|
+
let asserts = 0;
|
|
5972
|
+
walkWithin(body, (node) => {
|
|
5973
|
+
if (node.type !== "CallExpression") return;
|
|
5974
|
+
calls += 1;
|
|
5975
|
+
if (calleeNameOf(rootCallOf(node)) === ASSERT) asserts += 1;
|
|
5976
|
+
});
|
|
5977
|
+
return calls > 0 && calls === asserts;
|
|
5978
|
+
};
|
|
5979
|
+
var promisesIn = (scope) => {
|
|
5980
|
+
const created = /* @__PURE__ */ new Map();
|
|
5981
|
+
const proven = /* @__PURE__ */ new Set();
|
|
5982
|
+
walkWithin(scope, (node) => {
|
|
5983
|
+
if (node.type === "VariableDeclarator") {
|
|
5984
|
+
const id = node.id;
|
|
5985
|
+
const init = node.init;
|
|
5986
|
+
if (id?.type === "Identifier" && init?.type === "CallExpression") {
|
|
5987
|
+
created.set(id.name, init);
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
if (node.type === "AwaitExpression") {
|
|
5991
|
+
const argument = node.argument;
|
|
5992
|
+
if (argument?.type === "Identifier") proven.add(argument.name);
|
|
5993
|
+
}
|
|
5994
|
+
if (node.type === "MemberExpression") {
|
|
5995
|
+
const object = node.object;
|
|
5996
|
+
const property = propertyNameOf(node);
|
|
5997
|
+
if (object?.type === "Identifier" && (property === "then" || property === "catch" || property === "finally")) {
|
|
5998
|
+
proven.add(object.name);
|
|
5999
|
+
}
|
|
6000
|
+
}
|
|
6001
|
+
if (node.type === "CallExpression") {
|
|
6002
|
+
const object = node.callee?.object;
|
|
6003
|
+
if (object?.type === "Identifier" && object.name === "Promise") {
|
|
6004
|
+
for (const argument of node.arguments ?? []) {
|
|
6005
|
+
for (const element of argument.elements ?? [argument]) {
|
|
6006
|
+
if (element?.type === "Identifier") proven.add(element.name);
|
|
6007
|
+
}
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
}
|
|
6011
|
+
});
|
|
6012
|
+
return new Set([...created.keys()].filter((name) => proven.has(name)));
|
|
6013
|
+
};
|
|
6014
|
+
var testNoClockPollingForPromiseFlips = {
|
|
6015
|
+
create(context) {
|
|
6016
|
+
const options = bagOf(context.options);
|
|
6017
|
+
const pollers = options.pollers ?? POLLERS;
|
|
6018
|
+
const globs = options.testGlobs ?? TEST_GLOBS;
|
|
6019
|
+
const filename = normalise(context.filename);
|
|
6020
|
+
if (!globs.some((pattern) => new RegExp(pattern).test(filename))) return {};
|
|
6021
|
+
const inScope = (node) => {
|
|
6022
|
+
const held = promisesIn(node);
|
|
6023
|
+
if (held.size === 0) return;
|
|
6024
|
+
const [promise] = [...held].toSorted();
|
|
6025
|
+
walkWithin(node, (inner) => {
|
|
6026
|
+
if (inner.type !== "CallExpression") return;
|
|
6027
|
+
if (!pollers.includes(calleeNameOf(inner) ?? "")) return;
|
|
6028
|
+
const [callback] = inner.arguments ?? [];
|
|
6029
|
+
if (callback === void 0 || !FUNCTIONS2.has(callback.type ?? "")) return;
|
|
6030
|
+
if (!onlyAsserts(callback.body)) return;
|
|
6031
|
+
context.report({
|
|
6032
|
+
data: { poller: calleeNameOf(inner) ?? "", promise: promise ?? "" },
|
|
6033
|
+
messageId: "clockPoll",
|
|
6034
|
+
node: inner
|
|
6035
|
+
});
|
|
6036
|
+
});
|
|
6037
|
+
};
|
|
6038
|
+
return {
|
|
6039
|
+
ArrowFunctionExpression: inScope,
|
|
6040
|
+
FunctionDeclaration: inScope,
|
|
6041
|
+
FunctionExpression: inScope
|
|
6042
|
+
};
|
|
6043
|
+
},
|
|
6044
|
+
fixShape: `A test that already holds a promise awaits it; it does not poll a clock for the flip. A
|
|
6045
|
+
\`waitFor\` whose body is nothing but \`expect\` assertions, in a test that created a promise and proves
|
|
6046
|
+
it is one by awaiting or chaining it, is asking the scheduler a question the promise answers exactly:
|
|
6047
|
+
move the \`await\` above the assertions, or drain microtasks, and assert once. Keep \`waitFor\` for state
|
|
6048
|
+
nothing in the test can await \u2014 an element a click will render, a timer the runtime owns. Which files
|
|
6049
|
+
are tests is \`testGlobs\` and which calls poll is \`pollers\`, both defaulted.`,
|
|
6050
|
+
meta: {
|
|
6051
|
+
docs: {
|
|
6052
|
+
description: "In a test file, a `waitFor` whose callback only asserts, in a scope that already holds a promise, is a clock poll for a flip the promise decides. Await the promise instead."
|
|
6053
|
+
},
|
|
6054
|
+
messages: {
|
|
6055
|
+
clockPoll: 'This {{poller}} only asserts, and the test already holds the promise "{{promise}}". Await "{{promise}}" before asserting rather than polling the clock for the flip it decides \u2014 a poll that passes is a poll that was slower than the microtask, and a poll that fails is a timeout with no cause.'
|
|
6056
|
+
},
|
|
6057
|
+
schema: [
|
|
6058
|
+
{
|
|
6059
|
+
additionalProperties: false,
|
|
6060
|
+
properties: {
|
|
6061
|
+
pollers: { items: { type: "string" }, type: "array" },
|
|
6062
|
+
testGlobs: { items: { type: "string" }, type: "array" }
|
|
6063
|
+
},
|
|
6064
|
+
type: "object"
|
|
6065
|
+
}
|
|
6066
|
+
],
|
|
6067
|
+
type: "problem"
|
|
6068
|
+
}
|
|
6069
|
+
};
|
|
6070
|
+
var test_no_clock_polling_for_promise_flips_default = testNoClockPollingForPromiseFlips;
|
|
6071
|
+
|
|
5367
6072
|
// src/rules/time-through-the-door.ts
|
|
5368
6073
|
var RULE20 = "time-through-the-door";
|
|
5369
6074
|
var ACCEPTS6 = "{ door: string (a regex over the filename), exempt: string[] (regex sources, default the test paths: __tests__/, .test., .spec.), libraries: string[] (default ['dayjs']) }";
|
|
@@ -5395,6 +6100,7 @@ var timeThroughTheDoor = {
|
|
|
5395
6100
|
create(context) {
|
|
5396
6101
|
const door = requireOption(RULE20, context.options?.[0]?.door, "door", ACCEPTS6);
|
|
5397
6102
|
const exempt = context.options?.[0]?.exempt ?? DEFAULT_EXEMPT;
|
|
6103
|
+
const exemptModules = context.options?.[0]?.exemptModules ?? [];
|
|
5398
6104
|
const libraries = requireOption(
|
|
5399
6105
|
RULE20,
|
|
5400
6106
|
context.options?.[0]?.libraries ?? DEFAULT_LIBRARIES,
|
|
@@ -5403,6 +6109,7 @@ var timeThroughTheDoor = {
|
|
|
5403
6109
|
);
|
|
5404
6110
|
const filename = normalise(context.filename);
|
|
5405
6111
|
if (isDoor(filename, door)) return {};
|
|
6112
|
+
if (exemptModules.some((module) => isModule(filename, module))) return {};
|
|
5406
6113
|
if (exempt.some((pattern) => new RegExp(pattern).test(filename))) return {};
|
|
5407
6114
|
const where = readable2(door);
|
|
5408
6115
|
return {
|
|
@@ -5444,7 +6151,9 @@ the plugins, the zone and the formats. Import \`isoDate\` / \`today\` / \`addDay
|
|
|
5444
6151
|
operation there. A second module that imports the library is a second configured library: plugin
|
|
5445
6152
|
state is global, so the two disagree the moment one of them extends something the other did not.
|
|
5446
6153
|
Name the library in \`libraries\` (default \`['dayjs']\`) \u2014 a repo on date-fns or luxon says so and
|
|
5447
|
-
gets the whole rule rather than its raw-\`Date\` half.
|
|
6154
|
+
gets the whole rule rather than its raw-\`Date\` half. Name the modules that are outside the door in
|
|
6155
|
+
\`exemptModules\`, one per entry: \`exempt\` still takes patterns and still grants its exemption to
|
|
6156
|
+
wherever the pattern stops, which is a boundary nobody decided and nobody counts.
|
|
5448
6157
|
|
|
5449
6158
|
Three kinds of time, never mixed. An INSTANT is epoch milliseconds. A BUSINESS DATE is an ISO date
|
|
5450
6159
|
string, \`YYYY-MM-DD\`, parsed strictly in UTC \u2014 a calendar day is not an instant and rendering one as
|
|
@@ -5455,6 +6164,12 @@ The clock is injected, never ambient. Take \`nowMs: number\` as a parameter and
|
|
|
5455
6164
|
root pass \`Date.now()\` in \u2014 \`Date.now()\` is the one call allowed anywhere, and a function that reads
|
|
5456
6165
|
it internally cannot be tested at a chosen instant.`,
|
|
5457
6166
|
meta: {
|
|
6167
|
+
/**
|
|
6168
|
+
* #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
|
|
6169
|
+
* doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
|
|
6170
|
+
* grant nothing counts, which is the defect this pair exists to end.
|
|
6171
|
+
*/
|
|
6172
|
+
grants: { modules: "exemptModules", paths: "exempt" },
|
|
5458
6173
|
docs: {
|
|
5459
6174
|
description: "Time is handled behind one module \u2014 the `door` option names it \u2014 and nothing else constructs, parses or formats a Date or imports dayjs. Date.now() is the one call allowed everywhere. Enabled without a `door` it refuses the run."
|
|
5460
6175
|
},
|
|
@@ -5468,6 +6183,7 @@ it internally cannot be tested at a chosen instant.`,
|
|
|
5468
6183
|
properties: {
|
|
5469
6184
|
door: { type: "string" },
|
|
5470
6185
|
exempt: { items: { type: "string" }, type: "array" },
|
|
6186
|
+
exemptModules: { items: { type: "string" }, type: "array" },
|
|
5471
6187
|
libraries: { items: { type: "string" }, type: "array" }
|
|
5472
6188
|
},
|
|
5473
6189
|
type: "object"
|
|
@@ -5976,7 +6692,7 @@ export const geonosisProbeTodo = 1
|
|
|
5976
6692
|
var todo_names_its_plan_default = todoNamesItsPlan;
|
|
5977
6693
|
|
|
5978
6694
|
// src/rules/lib/colors.ts
|
|
5979
|
-
var
|
|
6695
|
+
var FUNCTIONS3 = [
|
|
5980
6696
|
"color",
|
|
5981
6697
|
"color-mix",
|
|
5982
6698
|
"hsl",
|
|
@@ -5990,7 +6706,7 @@ var FUNCTIONS = [
|
|
|
5990
6706
|
"rgba"
|
|
5991
6707
|
];
|
|
5992
6708
|
var HEX = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
5993
|
-
var FUNCTION_CALL = new RegExp(`\\b(?:${
|
|
6709
|
+
var FUNCTION_CALL = new RegExp(`\\b(?:${FUNCTIONS3.join("|")})\\(`, "i");
|
|
5994
6710
|
var HEX_ANYWHERE = /(?:^|[\s(,])#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i;
|
|
5995
6711
|
var NAMED2 = /* @__PURE__ */ new Set([
|
|
5996
6712
|
"aliceblue",
|
|
@@ -6147,7 +6863,7 @@ var isColorLiteral = (value) => {
|
|
|
6147
6863
|
if (trimmed === "") return false;
|
|
6148
6864
|
if (HEX.test(trimmed)) return true;
|
|
6149
6865
|
if (NAMED2.has(trimmed.toLowerCase())) return true;
|
|
6150
|
-
return new RegExp(`^(?:${
|
|
6866
|
+
return new RegExp(`^(?:${FUNCTIONS3.join("|")})\\(`, "i").test(trimmed);
|
|
6151
6867
|
};
|
|
6152
6868
|
var containsColorLiteral = (value) => HEX_ANYWHERE.test(value) || FUNCTION_CALL.test(value);
|
|
6153
6869
|
var COLOR_PROPERTIES = /* @__PURE__ */ new Set([
|
|
@@ -6232,10 +6948,11 @@ var tokenOffence = (value, prefixes, local) => {
|
|
|
6232
6948
|
};
|
|
6233
6949
|
var tokensOnlyColors = {
|
|
6234
6950
|
create(context) {
|
|
6235
|
-
const { allow = [], tokenPrefix } = context.options?.[0] ?? {};
|
|
6951
|
+
const { allow = [], exemptModules = [], tokenPrefix } = context.options?.[0] ?? {};
|
|
6236
6952
|
const prefixes = [requireOption("tokens-only-colors", tokenPrefix, "tokenPrefix")].flat();
|
|
6237
6953
|
requireOption("tokens-only-colors", prefixes, "tokenPrefix");
|
|
6238
6954
|
const filename = normalise(context.filename);
|
|
6955
|
+
if (exemptModules.some((module) => isModule(filename, module))) return {};
|
|
6239
6956
|
if (allow.some((path2) => filename.includes(path2))) return {};
|
|
6240
6957
|
return {
|
|
6241
6958
|
JSXOpeningElement(node) {
|
|
@@ -6269,11 +6986,19 @@ var tokensOnlyColors = {
|
|
|
6269
6986
|
\`style={{ color: 'var(--mc-primary)' }}\`, \`className="bg-brand-primary"\`. A \`var()\` that is NOT a
|
|
6270
6987
|
token (\`var(--legacy-blue)\`) is the same offence spelled differently: it reaches around the theme, so
|
|
6271
6988
|
the brand switches and the component does not \u2014 add the value to the theme and point at it there.
|
|
6272
|
-
Configure \`tokenPrefix\` with the prefixes your themed properties use, and
|
|
6273
|
-
in \`
|
|
6989
|
+
Configure \`tokenPrefix\` with the prefixes your themed properties use, and NAME the token layer's
|
|
6990
|
+
modules in \`exemptModules\`, one per entry, because that is the one place a literal belongs \u2014
|
|
6991
|
+
\`allow\` still takes patterns and still grants its exemption to wherever the pattern stops, which is
|
|
6992
|
+
a boundary nobody decided and nobody counts. Enabled with no \`tokenPrefix\` the rule
|
|
6274
6993
|
refuses the run: it cannot tell a token from a foreign variable, and a rule that cannot tell reads
|
|
6275
6994
|
exactly like a clean tree.`,
|
|
6276
6995
|
meta: {
|
|
6996
|
+
/**
|
|
6997
|
+
* #139: which option grants by PATTERN and which one ENUMERATES. Declared on the rule so the
|
|
6998
|
+
* doctor needs no list of its own — a rule that gains a path grant and forgets to say so is a
|
|
6999
|
+
* grant nothing counts, which is the defect this pair exists to end.
|
|
7000
|
+
*/
|
|
7001
|
+
grants: { modules: "exemptModules", paths: "allow" },
|
|
6277
7002
|
docs: {
|
|
6278
7003
|
description: "Colours in a component come from the token layer. A colour literal in a className or an inline style fires, and so does a var() naming a custom property outside the configured token prefixes. Configure `tokenPrefix`; put the token layer in `allow`. Enabled without `tokenPrefix` it refuses the run."
|
|
6279
7004
|
},
|
|
@@ -6286,6 +7011,7 @@ exactly like a clean tree.`,
|
|
|
6286
7011
|
additionalProperties: false,
|
|
6287
7012
|
properties: {
|
|
6288
7013
|
allow: { items: { type: "string" }, type: "array" },
|
|
7014
|
+
exemptModules: { items: { type: "string" }, type: "array" },
|
|
6289
7015
|
tokenPrefix: {
|
|
6290
7016
|
anyOf: [
|
|
6291
7017
|
{ type: "string" },
|
|
@@ -6666,6 +7392,7 @@ var rules = refusingOnce(
|
|
|
6666
7392
|
"emit-declares-attempts": emit_declares_attempts_default,
|
|
6667
7393
|
"font-roles-only": font_roles_only_default,
|
|
6668
7394
|
"layer-walls": layer_walls_default,
|
|
7395
|
+
"list-take-without-order": list_take_without_order_default,
|
|
6669
7396
|
"max-comment-density": max_comment_density_default,
|
|
6670
7397
|
"molecule-atoms-only": molecule_atoms_only_default,
|
|
6671
7398
|
"molecule-must-compose": molecule_must_compose_default,
|
|
@@ -6679,7 +7406,9 @@ var rules = refusingOnce(
|
|
|
6679
7406
|
"no-card-shaped-div": no_card_shaped_div_default,
|
|
6680
7407
|
"no-cross-feature-stores": no_cross_feature_stores_default,
|
|
6681
7408
|
"no-d1-transaction": no_d1_transaction_default,
|
|
7409
|
+
"no-date-string-compare": no_date_string_compare_default,
|
|
6682
7410
|
"no-duplicate-jsx-patterns": no_duplicate_jsx_patterns_default,
|
|
7411
|
+
"no-generic-utility-package": no_generic_utility_package_default,
|
|
6683
7412
|
"no-hook-in-component-disguise": no_hook_in_component_disguise_default,
|
|
6684
7413
|
"no-index-count-as-exact": no_index_count_as_exact_default,
|
|
6685
7414
|
"no-inert-hidden-jsx": no_inert_hidden_jsx_default,
|
|
@@ -6694,6 +7423,8 @@ var rules = refusingOnce(
|
|
|
6694
7423
|
"no-react-namespace": no_react_namespace_default,
|
|
6695
7424
|
"no-renamed-html-props": no_renamed_html_props_default,
|
|
6696
7425
|
"no-render-prop-reader": no_render_prop_reader_default,
|
|
7426
|
+
"no-swallowed-catch": no_swallowed_catch_default,
|
|
7427
|
+
"no-runtime-dead-affordance": no_runtime_dead_affordance_default,
|
|
6697
7428
|
"no-trivial-wrapper-component": no_trivial_wrapper_component_default,
|
|
6698
7429
|
"no-ts-in-bio-folders": no_ts_in_bio_folders_default,
|
|
6699
7430
|
"no-type-definitions-in-components": no_type_definitions_in_components_default,
|
|
@@ -6714,6 +7445,7 @@ var rules = refusingOnce(
|
|
|
6714
7445
|
"subscriber-declares-id": subscriber_declares_id_default,
|
|
6715
7446
|
"tables-declare-their-plane": tables_declare_their_plane_default,
|
|
6716
7447
|
"tenant-tables-carry-org-id": tenant_tables_carry_org_id_default,
|
|
7448
|
+
"test-no-clock-polling-for-promise-flips": test_no_clock_polling_for_promise_flips_default,
|
|
6717
7449
|
"time-through-the-door": time_through_the_door_default,
|
|
6718
7450
|
"tokens-only-colors": tokens_only_colors_default,
|
|
6719
7451
|
"tissue-must-compose": tissue_must_compose_default,
|