@jterrazz/test 15.0.0 → 15.1.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 +1 -1
- package/dist/ambiguity.js +1 -1
- package/dist/appium.adapter.js +33 -24
- package/dist/artifacts.js +503 -525
- package/dist/checker.js +60 -59
- package/dist/index.d.ts +587 -660
- package/dist/index.js +191 -188
- package/dist/match.js +6 -16
- package/dist/oxlint.cjs +538 -525
- package/dist/oxlint.d.cts +7064 -15
- package/dist/oxlint.d.ts +7064 -15
- package/dist/oxlint.js +538 -525
- package/dist/playwright.adapter.js +36 -14
- package/dist/queue.js +27 -28
- package/dist/spec-document.js +19 -25
- package/dist/vitest.d.ts +6 -6
- package/dist/vitest.js +2 -2
- package/package.json +10 -7
package/dist/oxlint.cjs
CHANGED
|
@@ -4,7 +4,6 @@ Object.defineProperties(exports, {
|
|
|
4
4
|
});
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
|
-
let node_url = require("node:url");
|
|
8
7
|
//#region src/lint/fs-cache.ts
|
|
9
8
|
/**
|
|
10
9
|
* Cached filesystem probes for the fs-anchored rules (a9w, c2, c6, c7, i2).
|
|
@@ -70,7 +69,7 @@ function readFileCached(path) {
|
|
|
70
69
|
*/
|
|
71
70
|
/** Split a path into its non-empty segments (posix or win separators). */
|
|
72
71
|
function segments(path) {
|
|
73
|
-
return path.split(/[/\\]/).filter(Boolean);
|
|
72
|
+
return path.split(/[/\\]/u).filter(Boolean);
|
|
74
73
|
}
|
|
75
74
|
/**
|
|
76
75
|
* The `specs/` directory a file belongs to: the NEAREST ancestor named `specs`,
|
|
@@ -112,7 +111,8 @@ function stringValue(node) {
|
|
|
112
111
|
if (node.type === "TemplateLiteral") {
|
|
113
112
|
const expressions = node.expressions;
|
|
114
113
|
const quasis = node.quasis;
|
|
115
|
-
|
|
114
|
+
const onlyQuasi = quasis?.[0];
|
|
115
|
+
if (expressions?.length === 0 && quasis?.length === 1 && onlyQuasi !== void 0) return onlyQuasi.value?.cooked;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
/** The property name of a non-computed member expression, if identifiable. */
|
|
@@ -483,7 +483,7 @@ const RULE_DOCS = {
|
|
|
483
483
|
},
|
|
484
484
|
"f3-specs-public-entry": {
|
|
485
485
|
channel: "statique",
|
|
486
|
-
convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{
|
|
486
|
+
convention: "Depuis `specs/`, seul l’import en profondeur des INTERNES du framework est interdit : un chemin relatif résolvant dans `src/{specification,integrations,vitest,lint}/` du dépôt du framework, ou tout `@jterrazz/test/<subpath>` que la map `exports` du paquet ne publie pas — les subpaths publiés sont exemptés, LUS du manifeste comme en F1, qui en tient la liste. Les imports de la source de SA PROPRE app par un consommateur sont toujours permis (c’est le motif documenté) ; seul `specs/integrations/` peut importer en profondeur `src/integrations/**`.",
|
|
487
487
|
family: "F",
|
|
488
488
|
id: "F3",
|
|
489
489
|
rationale: "Tester par la surface publique garde les specs découplées des chemins internes du framework, sans gêner le consommateur qui importe sa propre app."
|
|
@@ -893,83 +893,6 @@ const a1SpecificationFile = {
|
|
|
893
893
|
}
|
|
894
894
|
};
|
|
895
895
|
//#endregion
|
|
896
|
-
//#region src/core/specification/shared/binding.ts
|
|
897
|
-
/**
|
|
898
|
-
* Compose-binding resolution and the case conversions it shares with the
|
|
899
|
-
* automatic env injection (CONVENTIONS A6 / B6).
|
|
900
|
-
*
|
|
901
|
-
* A services-record key is written in the natural TypeScript style
|
|
902
|
-
* (`analyticsDb`), while a compose service name is kebab-case
|
|
903
|
-
* (`analytics-db`). These helpers bridge the two vocabularies deterministically
|
|
904
|
-
* so the record key stays the single source of truth and `composeService`
|
|
905
|
-
* remains a rare escape hatch.
|
|
906
|
-
*/
|
|
907
|
-
/**
|
|
908
|
-
* Convert a record key to its kebab-case form: camelCase boundaries become
|
|
909
|
-
* `-`, everything is lowercased. `analyticsDb` → `analytics-db`, `db` → `db`.
|
|
910
|
-
*/
|
|
911
|
-
function toKebabCase(key) {
|
|
912
|
-
return key.replace(/(?<lower>[a-z0-9])(?<upper>[A-Z])/g, "$<lower>-$<upper>").toLowerCase();
|
|
913
|
-
}
|
|
914
|
-
//#endregion
|
|
915
|
-
//#region src/lint/rules/a10-duplicate-binding.ts
|
|
916
|
-
/** The compose service an entry binds to: explicit `composeService`, else the key's kebab form. */
|
|
917
|
-
function canonicalBinding(entry, key) {
|
|
918
|
-
const factory = entry.value;
|
|
919
|
-
if (factory?.type === "CallExpression") {
|
|
920
|
-
const options = factory.arguments?.[0];
|
|
921
|
-
if (options?.type === "ObjectExpression") {
|
|
922
|
-
const explicit = stringValue(findProperty(options, "composeService")?.value);
|
|
923
|
-
if (explicit !== void 0) return explicit;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return toKebabCase(key);
|
|
927
|
-
}
|
|
928
|
-
/**
|
|
929
|
-
* CONVENTIONS A10 — within one `services` record, two keys that derive (or
|
|
930
|
-
* pin via `composeService`) the SAME compose service name are a silent
|
|
931
|
-
* collision: the record is a map, and the second binding would shadow the
|
|
932
|
-
* first. Detected on the same kebab derivation the runtime binding uses
|
|
933
|
-
* (`analyticsDb` and `analytics-db` both → `analytics-db`), and across explicit
|
|
934
|
-
* `composeService` values.
|
|
935
|
-
*/
|
|
936
|
-
const a10DuplicateBinding = {
|
|
937
|
-
create(context) {
|
|
938
|
-
return { CallExpression(node) {
|
|
939
|
-
if (specificationMember(node) === void 0) return;
|
|
940
|
-
for (const argument of node.arguments ?? []) {
|
|
941
|
-
const record = findProperty(argument, "services")?.value;
|
|
942
|
-
if (record?.type !== "ObjectExpression") continue;
|
|
943
|
-
const seen = /* @__PURE__ */ new Map();
|
|
944
|
-
for (const entry of record.properties ?? []) {
|
|
945
|
-
const key = propertyKeyName(entry);
|
|
946
|
-
if (key === void 0) continue;
|
|
947
|
-
const binding = canonicalBinding(entry, key);
|
|
948
|
-
const first = seen.get(binding);
|
|
949
|
-
if (first !== void 0) {
|
|
950
|
-
context.report({
|
|
951
|
-
data: {
|
|
952
|
-
binding,
|
|
953
|
-
first,
|
|
954
|
-
key
|
|
955
|
-
},
|
|
956
|
-
messageId: "duplicate",
|
|
957
|
-
node: entry
|
|
958
|
-
});
|
|
959
|
-
continue;
|
|
960
|
-
}
|
|
961
|
-
seen.set(binding, key);
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
} };
|
|
965
|
-
},
|
|
966
|
-
meta: {
|
|
967
|
-
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
968
|
-
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/13-linting.md)." },
|
|
969
|
-
type: "problem"
|
|
970
|
-
}
|
|
971
|
-
};
|
|
972
|
-
//#endregion
|
|
973
896
|
//#region src/lint/rules/a2-known-constructors.ts
|
|
974
897
|
/** The five constructors, and only five (A2 — see docs/13-linting.md). */
|
|
975
898
|
const KNOWN_CONSTRUCTORS = /* @__PURE__ */ new Set([
|
|
@@ -1132,6 +1055,25 @@ const a5ModeWithServer = {
|
|
|
1132
1055
|
}
|
|
1133
1056
|
};
|
|
1134
1057
|
//#endregion
|
|
1058
|
+
//#region src/specification/facets/_common/binding.ts
|
|
1059
|
+
/**
|
|
1060
|
+
* Compose-binding resolution and the case conversions it shares with the
|
|
1061
|
+
* automatic env injection (CONVENTIONS A6 / B6).
|
|
1062
|
+
*
|
|
1063
|
+
* A services-record key is written in the natural TypeScript style
|
|
1064
|
+
* (`analyticsDb`), while a compose service name is kebab-case
|
|
1065
|
+
* (`analytics-db`). These helpers bridge the two vocabularies deterministically
|
|
1066
|
+
* so the record key stays the single source of truth and `composeService`
|
|
1067
|
+
* remains a rare escape hatch.
|
|
1068
|
+
*/
|
|
1069
|
+
/**
|
|
1070
|
+
* Convert a record key to its kebab-case form: camelCase boundaries become
|
|
1071
|
+
* `-`, everything is lowercased. `analyticsDb` → `analytics-db`, `db` → `db`.
|
|
1072
|
+
*/
|
|
1073
|
+
function toKebabCase(key) {
|
|
1074
|
+
return key.replaceAll(/(?<lower>[a-z0-9])(?<upper>[A-Z])/gu, "$<lower>-$<upper>").toLowerCase();
|
|
1075
|
+
}
|
|
1076
|
+
//#endregion
|
|
1135
1077
|
//#region src/lint/rules/a6w-redundant-compose-service.ts
|
|
1136
1078
|
/**
|
|
1137
1079
|
* CONVENTIONS A6 (warning) — `composeService:` is the escape hatch for
|
|
@@ -1173,7 +1115,7 @@ const a6wRedundantComposeService = {
|
|
|
1173
1115
|
}
|
|
1174
1116
|
};
|
|
1175
1117
|
//#endregion
|
|
1176
|
-
//#region src/
|
|
1118
|
+
//#region src/specification/facets/_common/resolve.ts
|
|
1177
1119
|
/** What makes a directory a project root — a manifest, or a test stack. */
|
|
1178
1120
|
const ROOT_MARKERS = ["package.json", "docker/compose.test.yaml"];
|
|
1179
1121
|
/**
|
|
@@ -1240,7 +1182,65 @@ const a9wRedundantRoot = {
|
|
|
1240
1182
|
}
|
|
1241
1183
|
};
|
|
1242
1184
|
//#endregion
|
|
1243
|
-
//#region src/
|
|
1185
|
+
//#region src/lint/rules/a10-duplicate-binding.ts
|
|
1186
|
+
/** The compose service an entry binds to: explicit `composeService`, else the key's kebab form. */
|
|
1187
|
+
function canonicalBinding(entry, key) {
|
|
1188
|
+
const factory = entry.value;
|
|
1189
|
+
if (factory?.type === "CallExpression") {
|
|
1190
|
+
const options = factory.arguments?.[0];
|
|
1191
|
+
if (options?.type === "ObjectExpression") {
|
|
1192
|
+
const explicit = stringValue(findProperty(options, "composeService")?.value);
|
|
1193
|
+
if (explicit !== void 0) return explicit;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return toKebabCase(key);
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* CONVENTIONS A10 — within one `services` record, two keys that derive (or
|
|
1200
|
+
* pin via `composeService`) the SAME compose service name are a silent
|
|
1201
|
+
* collision: the record is a map, and the second binding would shadow the
|
|
1202
|
+
* first. Detected on the same kebab derivation the runtime binding uses
|
|
1203
|
+
* (`analyticsDb` and `analytics-db` both → `analytics-db`), and across explicit
|
|
1204
|
+
* `composeService` values.
|
|
1205
|
+
*/
|
|
1206
|
+
const a10DuplicateBinding = {
|
|
1207
|
+
create(context) {
|
|
1208
|
+
return { CallExpression(node) {
|
|
1209
|
+
if (specificationMember(node) === void 0) return;
|
|
1210
|
+
for (const argument of node.arguments ?? []) {
|
|
1211
|
+
const record = findProperty(argument, "services")?.value;
|
|
1212
|
+
if (record?.type !== "ObjectExpression") continue;
|
|
1213
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1214
|
+
for (const entry of record.properties ?? []) {
|
|
1215
|
+
const key = propertyKeyName(entry);
|
|
1216
|
+
if (key === void 0) continue;
|
|
1217
|
+
const binding = canonicalBinding(entry, key);
|
|
1218
|
+
const first = seen.get(binding);
|
|
1219
|
+
if (first !== void 0) {
|
|
1220
|
+
context.report({
|
|
1221
|
+
data: {
|
|
1222
|
+
binding,
|
|
1223
|
+
first,
|
|
1224
|
+
key
|
|
1225
|
+
},
|
|
1226
|
+
messageId: "duplicate",
|
|
1227
|
+
node: entry
|
|
1228
|
+
});
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
seen.set(binding, key);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
} };
|
|
1235
|
+
},
|
|
1236
|
+
meta: {
|
|
1237
|
+
docs: RULE_DOCS["a10-duplicate-binding"],
|
|
1238
|
+
messages: { duplicate: "Service keys \"{{first}}\" and \"{{key}}\" both bind to compose service \"{{binding}}\" — one shadows the other; rename a key or set distinct composeService values (A10 — see docs/13-linting.md)." },
|
|
1239
|
+
type: "problem"
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
//#endregion
|
|
1243
|
+
//#region src/specification/facets/_common/ground.ts
|
|
1244
1244
|
/**
|
|
1245
1245
|
* The GROUND a row of specs stands on — the naming law of a spec tree.
|
|
1246
1246
|
*
|
|
@@ -1281,7 +1281,7 @@ const RENAMED_GROUND_DIRS = {
|
|
|
1281
1281
|
seeds: GROUND_SEEDS
|
|
1282
1282
|
};
|
|
1283
1283
|
//#endregion
|
|
1284
|
-
//#region src/
|
|
1284
|
+
//#region src/specification/facets/_common/fixtures.ts
|
|
1285
1285
|
/**
|
|
1286
1286
|
* Fixture path resolution + copy semantics for the `cli` facet's `.fixture()`.
|
|
1287
1287
|
*
|
|
@@ -1328,7 +1328,7 @@ const b2KnownFixtureMarker = {
|
|
|
1328
1328
|
const args = node.arguments ?? [];
|
|
1329
1329
|
const path = leadingText(args[0]);
|
|
1330
1330
|
if (path === void 0 || !path.startsWith("$")) return;
|
|
1331
|
-
const marker = path.replace(
|
|
1331
|
+
const marker = path.replace(/\/+$/u, "").split("/")[0] ?? "";
|
|
1332
1332
|
if (!KNOWN.has(marker)) context.report({
|
|
1333
1333
|
data: {
|
|
1334
1334
|
known: [...KNOWN_FIXTURE_MARKERS].join(", "),
|
|
@@ -1462,7 +1462,7 @@ const b5AwaitUsing = {
|
|
|
1462
1462
|
};
|
|
1463
1463
|
//#endregion
|
|
1464
1464
|
//#region src/lint/rules/b6w-redundant-env-url.ts
|
|
1465
|
-
const URL_KEY = /^[A-Z0-9]+(?:_[A-Z0-9]+)*_URL
|
|
1465
|
+
const URL_KEY = /^[A-Z0-9]+(?:_[A-Z0-9]+)*_URL$/u;
|
|
1466
1466
|
/**
|
|
1467
1467
|
* CONVENTIONS B6 (warning) — in `cli` mode with services, the framework
|
|
1468
1468
|
* already injects `<KEY>_URL` for every service (plus the unambiguous
|
|
@@ -1498,7 +1498,7 @@ const b6wRedundantEnvUrl = {
|
|
|
1498
1498
|
//#endregion
|
|
1499
1499
|
//#region src/lint/rules/b8-kebab-trigger.ts
|
|
1500
1500
|
/** Stable kebab-case job identifier: `nightly-report`, `send-welcome-emails`. */
|
|
1501
|
-
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)
|
|
1501
|
+
const KEBAB_CASE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1502
1502
|
/**
|
|
1503
1503
|
* CONVENTIONS B8 — a job `name` passed to `.trigger()` is a stable kebab-case
|
|
1504
1504
|
* identifier; it is a contract between the app and its tests. Only string
|
|
@@ -1754,212 +1754,71 @@ const c1DomainStructure = {
|
|
|
1754
1754
|
}
|
|
1755
1755
|
};
|
|
1756
1756
|
//#endregion
|
|
1757
|
-
//#region src/lint/rules/
|
|
1758
|
-
|
|
1759
|
-
const INTERNAL_UNIT = /(?:^|\/)contracts\/(?:anthropic|http|openai)\//u;
|
|
1757
|
+
//#region src/lint/rules/c2-http-only-requests.ts
|
|
1758
|
+
const TEST_FILE$12 = /\.test\.[cm]?[jt]sx?$/u;
|
|
1760
1759
|
/**
|
|
1761
|
-
* CONVENTIONS
|
|
1762
|
-
* a
|
|
1763
|
-
*
|
|
1764
|
-
*
|
|
1765
|
-
*
|
|
1766
|
-
* scenario is named once, next to the world it derives from.
|
|
1760
|
+
* CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
|
|
1761
|
+
* a COMPLETE request: method, path, headers, body). Anchored on the feature's
|
|
1762
|
+
* visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
|
|
1763
|
+
* `_requests/` directory is probed (cached readdir) and any non-`.http` entry
|
|
1764
|
+
* is reported on the test file.
|
|
1767
1765
|
*/
|
|
1768
|
-
const
|
|
1766
|
+
const c2HttpOnlyRequests = {
|
|
1769
1767
|
create(context) {
|
|
1770
|
-
const
|
|
1771
|
-
if (!
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
if (
|
|
1775
|
-
data: {
|
|
1776
|
-
messageId: "
|
|
1768
|
+
const file = context.physicalFilename;
|
|
1769
|
+
if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
|
|
1770
|
+
return { Program(node) {
|
|
1771
|
+
const requestsDir = (0, node_path.join)((0, node_path.dirname)(file), GROUND_REQUESTS);
|
|
1772
|
+
for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
|
|
1773
|
+
data: { entry },
|
|
1774
|
+
messageId: "notHttp",
|
|
1777
1775
|
node
|
|
1778
1776
|
});
|
|
1779
|
-
}
|
|
1777
|
+
} };
|
|
1780
1778
|
},
|
|
1781
1779
|
meta: {
|
|
1782
|
-
docs: RULE_DOCS["
|
|
1783
|
-
messages: {
|
|
1780
|
+
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1781
|
+
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
|
|
1784
1782
|
type: "problem"
|
|
1785
1783
|
}
|
|
1786
1784
|
};
|
|
1787
1785
|
//#endregion
|
|
1788
|
-
//#region src/lint/rules/
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
const PROVIDERS$1 = [
|
|
1786
|
+
//#region src/lint/rules/c4-contract-shape.ts
|
|
1787
|
+
/** The only directories a `contracts/` root may hold — the provider carriers. */
|
|
1788
|
+
const PROVIDERS$1 = /* @__PURE__ */ new Set([
|
|
1792
1789
|
"anthropic",
|
|
1793
1790
|
"http",
|
|
1794
1791
|
"openai"
|
|
1795
|
-
];
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1792
|
+
]);
|
|
1793
|
+
const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/u;
|
|
1794
|
+
/** A file oxlint itself visits — the AST half of the rule reports on those. */
|
|
1795
|
+
const SOURCE_FILE = /\.[cm]?[jt]sx?$/u;
|
|
1796
|
+
/** The public facade of a feature: `contracts/<kebab>.contracts.ts`. */
|
|
1797
|
+
const COMPOSITE_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.contracts\.[cm]?ts$/u;
|
|
1798
|
+
/** An internal unit contract: `contracts/<provider>/<kebab>.ts`. */
|
|
1799
|
+
const UNIT_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[cm]?ts$/u;
|
|
1800
|
+
/** Matched data next to its contract: `contracts/<provider>/<stem>.request.ts`. */
|
|
1801
|
+
const REQUEST_DATA_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.request\.[cm]?ts$/u;
|
|
1802
|
+
/** The name a specifier/declaration exports under, when it is `default`. */
|
|
1803
|
+
function exportsDefault(node) {
|
|
1804
|
+
return (node.specifiers ?? []).some((specifier) => {
|
|
1805
|
+
const exported = specifier.exported;
|
|
1806
|
+
return exported?.type === "Identifier" && exported.name === "default" || stringValue(exported) === "default";
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
/** Is this node a `defineContract(...)` call? */
|
|
1810
|
+
function isDefineContract(node) {
|
|
1811
|
+
const callee = node.callee;
|
|
1812
|
+
return node.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "defineContract";
|
|
1813
|
+
}
|
|
1814
|
+
/** A default export that may legitimately produce a contract (value or factory). */
|
|
1815
|
+
function producesContract(declaration) {
|
|
1816
|
+
if (declaration === void 0) return false;
|
|
1817
|
+
return isDefineContract(declaration) || declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionDeclaration" || declaration.type === "FunctionExpression" || declaration.type === "Identifier";
|
|
1801
1818
|
}
|
|
1802
1819
|
/**
|
|
1803
|
-
*
|
|
1804
|
-
*
|
|
1805
|
-
* pairs with the sibling `<stem>.ts` that serves it, where the stem is the name
|
|
1806
|
-
* up to the FIRST dot: `events.fr.response.json` belongs to `events.ts`. A data
|
|
1807
|
-
* file with no owner is dead weight no test can reach — reported on the feature's
|
|
1808
|
-
* test file, since oxlint never visits a `.json`.
|
|
1809
|
-
*/
|
|
1810
|
-
const c11ContractDataPairing = {
|
|
1811
|
-
create(context) {
|
|
1812
|
-
const file = context.physicalFilename;
|
|
1813
|
-
if (!TEST_FILE$12.test(file) || !segments(file).includes("specs")) return {};
|
|
1814
|
-
return { Program(node) {
|
|
1815
|
-
const contractsDir = (0, node_path.join)((0, node_path.dirname)(file), "contracts");
|
|
1816
|
-
for (const provider of PROVIDERS$1) {
|
|
1817
|
-
const entries = listDirectory((0, node_path.join)(contractsDir, provider));
|
|
1818
|
-
if (entries === null) continue;
|
|
1819
|
-
const present = new Set(entries);
|
|
1820
|
-
for (const entry of entries) {
|
|
1821
|
-
if (!DATA_SUFFIXES.some((suffix) => entry.endsWith(suffix))) continue;
|
|
1822
|
-
const owner = ownerOf(entry);
|
|
1823
|
-
if (!present.has(owner)) context.report({
|
|
1824
|
-
data: {
|
|
1825
|
-
entry,
|
|
1826
|
-
owner,
|
|
1827
|
-
provider
|
|
1828
|
-
},
|
|
1829
|
-
messageId: "orphan",
|
|
1830
|
-
node
|
|
1831
|
-
});
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
} };
|
|
1835
|
-
},
|
|
1836
|
-
meta: {
|
|
1837
|
-
docs: RULE_DOCS["c11-contract-data-pairing"],
|
|
1838
|
-
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/13-linting.md)." },
|
|
1839
|
-
type: "problem"
|
|
1840
|
-
}
|
|
1841
|
-
};
|
|
1842
|
-
//#endregion
|
|
1843
|
-
//#region src/lint/rules/c13-underscored-ground.ts
|
|
1844
|
-
const TEST_FILE$11 = /\.test\.[cm]?[jt]sx?$/;
|
|
1845
|
-
/**
|
|
1846
|
-
* CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
|
|
1847
|
-
* directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
|
|
1848
|
-
* `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
|
|
1849
|
-
* `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
|
|
1850
|
-
* fallback.
|
|
1851
|
-
*
|
|
1852
|
-
* The catch this rule exists for: a consumer that upgrades and keeps the old
|
|
1853
|
-
* names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
|
|
1854
|
-
* `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
|
|
1855
|
-
* and the tree becomes invisible rather than wrong. The failure that follows is
|
|
1856
|
-
* "fixture does not exist" on every spec at once, one step removed from the
|
|
1857
|
-
* cause; this rule states the cause and the rename.
|
|
1858
|
-
*
|
|
1859
|
-
* Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
|
|
1860
|
-
* spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
|
|
1861
|
-
* nearest specs root. A DOMAIN folder named after the ground it holds
|
|
1862
|
-
* (`specs/api/requests/_requests/`) is never a probe target — only what sits
|
|
1863
|
-
* beside a spec is.
|
|
1864
|
-
*/
|
|
1865
|
-
const c13UnderscoredGround = {
|
|
1866
|
-
create(context) {
|
|
1867
|
-
const file = context.physicalFilename;
|
|
1868
|
-
const anchor = specsAnchor(file);
|
|
1869
|
-
if (!TEST_FILE$11.test(file) || anchor === void 0) return {};
|
|
1870
|
-
return { Program(node) {
|
|
1871
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1872
|
-
const probe = (parent, legacy) => {
|
|
1873
|
-
const path = (0, node_path.join)(parent, legacy);
|
|
1874
|
-
if (seen.has(path) || !isDirectory(path)) return;
|
|
1875
|
-
seen.add(path);
|
|
1876
|
-
context.report({
|
|
1877
|
-
data: {
|
|
1878
|
-
ground: RENAMED_GROUND_DIRS[legacy],
|
|
1879
|
-
legacy
|
|
1880
|
-
},
|
|
1881
|
-
messageId: "notUnderscored",
|
|
1882
|
-
node
|
|
1883
|
-
});
|
|
1884
|
-
};
|
|
1885
|
-
for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe((0, node_path.dirname)(file), legacy);
|
|
1886
|
-
probe(anchor.directory, "fixtures");
|
|
1887
|
-
} };
|
|
1888
|
-
},
|
|
1889
|
-
meta: {
|
|
1890
|
-
docs: RULE_DOCS["c13-underscored-ground"],
|
|
1891
|
-
messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/13-linting.md)." },
|
|
1892
|
-
type: "problem"
|
|
1893
|
-
}
|
|
1894
|
-
};
|
|
1895
|
-
//#endregion
|
|
1896
|
-
//#region src/lint/rules/c2-http-only-requests.ts
|
|
1897
|
-
const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/;
|
|
1898
|
-
/**
|
|
1899
|
-
* CONVENTIONS C2 — `_requests/` contains only `.http` files (a request file is
|
|
1900
|
-
* a COMPLETE request: method, path, headers, body). Anchored on the feature's
|
|
1901
|
-
* visited test file: when `<feature>/<feature>.test.ts` is linted, its sibling
|
|
1902
|
-
* `_requests/` directory is probed (cached readdir) and any non-`.http` entry
|
|
1903
|
-
* is reported on the test file.
|
|
1904
|
-
*/
|
|
1905
|
-
const c2HttpOnlyRequests = {
|
|
1906
|
-
create(context) {
|
|
1907
|
-
const file = context.physicalFilename;
|
|
1908
|
-
if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
|
|
1909
|
-
return { Program(node) {
|
|
1910
|
-
const requestsDir = (0, node_path.join)((0, node_path.dirname)(file), GROUND_REQUESTS);
|
|
1911
|
-
for (const entry of listDirectory(requestsDir) ?? []) if (!entry.endsWith(".http")) context.report({
|
|
1912
|
-
data: { entry },
|
|
1913
|
-
messageId: "notHttp",
|
|
1914
|
-
node
|
|
1915
|
-
});
|
|
1916
|
-
} };
|
|
1917
|
-
},
|
|
1918
|
-
meta: {
|
|
1919
|
-
docs: RULE_DOCS["c2-http-only-requests"],
|
|
1920
|
-
messages: { notHttp: "_requests/{{entry}} is not a .http file — _requests/ contains only complete .http request files (C2 — see docs/13-linting.md)." },
|
|
1921
|
-
type: "problem"
|
|
1922
|
-
}
|
|
1923
|
-
};
|
|
1924
|
-
//#endregion
|
|
1925
|
-
//#region src/lint/rules/c4-contract-shape.ts
|
|
1926
|
-
/** The only directories a `contracts/` root may hold — the provider carriers. */
|
|
1927
|
-
const PROVIDERS = /* @__PURE__ */ new Set([
|
|
1928
|
-
"anthropic",
|
|
1929
|
-
"http",
|
|
1930
|
-
"openai"
|
|
1931
|
-
]);
|
|
1932
|
-
const TEST_FILE$9 = /\.test\.[cm]?[jt]sx?$/;
|
|
1933
|
-
/** A file oxlint itself visits — the AST half of the rule reports on those. */
|
|
1934
|
-
const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
|
|
1935
|
-
/** The public facade of a feature: `contracts/<kebab>.contracts.ts`. */
|
|
1936
|
-
const COMPOSITE_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.contracts\.[cm]?ts$/u;
|
|
1937
|
-
/** An internal unit contract: `contracts/<provider>/<kebab>.ts`. */
|
|
1938
|
-
const UNIT_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.[cm]?ts$/u;
|
|
1939
|
-
/** Matched data next to its contract: `contracts/<provider>/<stem>.request.ts`. */
|
|
1940
|
-
const REQUEST_DATA_FILE = /^[a-z0-9]+(?:-[a-z0-9]+)*\.request\.[cm]?ts$/u;
|
|
1941
|
-
/** Served data next to its contract: `contracts/<provider>/<stem>[.<qualifier>].response.json`. */
|
|
1942
|
-
const RESPONSE_DATA_FILE = /\.response\.json$/;
|
|
1943
|
-
/** The name a specifier/declaration exports under, when it is `default`. */
|
|
1944
|
-
function exportsDefault(node) {
|
|
1945
|
-
return (node.specifiers ?? []).some((specifier) => {
|
|
1946
|
-
const exported = specifier.exported;
|
|
1947
|
-
return exported?.type === "Identifier" && exported.name === "default" || stringValue(exported) === "default";
|
|
1948
|
-
});
|
|
1949
|
-
}
|
|
1950
|
-
/** Is this node a `defineContract(...)` call? */
|
|
1951
|
-
function isDefineContract(node) {
|
|
1952
|
-
const callee = node.callee;
|
|
1953
|
-
return node.type === "CallExpression" && callee?.type === "Identifier" && callee.name === "defineContract";
|
|
1954
|
-
}
|
|
1955
|
-
/** A default export that may legitimately produce a contract (value or factory). */
|
|
1956
|
-
function producesContract(declaration) {
|
|
1957
|
-
if (declaration === void 0) return false;
|
|
1958
|
-
return isDefineContract(declaration) || declaration.type === "ArrowFunctionExpression" || declaration.type === "FunctionDeclaration" || declaration.type === "FunctionExpression" || declaration.type === "Identifier";
|
|
1959
|
-
}
|
|
1960
|
-
/**
|
|
1961
|
-
* The provider a `request:` value implies, when the builder is written as
|
|
1962
|
-
* `<provider>.<verb>(…)` — the only statically decidable form.
|
|
1820
|
+
* The provider a `request:` value implies, when the builder is written as
|
|
1821
|
+
* `<provider>.<verb>(…)` — the only statically decidable form.
|
|
1963
1822
|
*/
|
|
1964
1823
|
function requestProvider(request) {
|
|
1965
1824
|
if (request?.type !== "CallExpression") return;
|
|
@@ -1987,7 +1846,7 @@ function checkLayout(context, node, contractsDir) {
|
|
|
1987
1846
|
});
|
|
1988
1847
|
continue;
|
|
1989
1848
|
}
|
|
1990
|
-
if (!PROVIDERS.has(entry)) {
|
|
1849
|
+
if (!PROVIDERS$1.has(entry)) {
|
|
1991
1850
|
context.report({
|
|
1992
1851
|
data: { entry },
|
|
1993
1852
|
messageId: "badProviderDir",
|
|
@@ -2003,7 +1862,7 @@ function checkLayout(context, node, contractsDir) {
|
|
|
2003
1862
|
messageId: "providerSubfolder",
|
|
2004
1863
|
node
|
|
2005
1864
|
});
|
|
2006
|
-
else if (!/\.[cm]?ts$/u.test(child) && !
|
|
1865
|
+
else if (!/\.[cm]?ts$/u.test(child) && !child.endsWith(".response.json")) context.report({
|
|
2007
1866
|
data: {
|
|
2008
1867
|
child,
|
|
2009
1868
|
provider: entry
|
|
@@ -2072,7 +1931,7 @@ function checkUnit(context, program, base, provider) {
|
|
|
2072
1931
|
if (isDefineContract(node)) {
|
|
2073
1932
|
const argument = node.arguments?.[0];
|
|
2074
1933
|
const declared = requestProvider(argument === void 0 ? void 0 : findProperty(argument, "request")?.value);
|
|
2075
|
-
if (declared !== void 0 && PROVIDERS.has(declared) && declared !== provider) context.report({
|
|
1934
|
+
if (declared !== void 0 && PROVIDERS$1.has(declared) && declared !== provider) context.report({
|
|
2076
1935
|
data: {
|
|
2077
1936
|
declared,
|
|
2078
1937
|
provider
|
|
@@ -2114,7 +1973,7 @@ const c4ContractShape = {
|
|
|
2114
1973
|
if (!parts.includes("specs")) return {};
|
|
2115
1974
|
const contractsIndex = parts.lastIndexOf("contracts");
|
|
2116
1975
|
if (contractsIndex === -1) {
|
|
2117
|
-
if (!TEST_FILE$
|
|
1976
|
+
if (!TEST_FILE$11.test(file)) return {};
|
|
2118
1977
|
return { Program(node) {
|
|
2119
1978
|
checkLayout(context, node, (0, node_path.join)((0, node_path.dirname)(file), "contracts"));
|
|
2120
1979
|
} };
|
|
@@ -2125,8 +1984,8 @@ const c4ContractShape = {
|
|
|
2125
1984
|
checkComposite(context, node, base);
|
|
2126
1985
|
} };
|
|
2127
1986
|
if (depth === 2) {
|
|
2128
|
-
const provider = parts[contractsIndex + 1];
|
|
2129
|
-
return PROVIDERS.has(provider) ? { Program(node) {
|
|
1987
|
+
const provider = parts[contractsIndex + 1] ?? "";
|
|
1988
|
+
return PROVIDERS$1.has(provider) ? { Program(node) {
|
|
2130
1989
|
checkUnit(context, node, base, provider);
|
|
2131
1990
|
} } : {};
|
|
2132
1991
|
}
|
|
@@ -2159,7 +2018,7 @@ const c4ContractShape = {
|
|
|
2159
2018
|
};
|
|
2160
2019
|
//#endregion
|
|
2161
2020
|
//#region src/lint/rules/c6-tomatch-extension.ts
|
|
2162
|
-
const TEST_FILE$
|
|
2021
|
+
const TEST_FILE$10 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2163
2022
|
/** Does the expect(…) subject chain contain a `.directory(…)` call? */
|
|
2164
2023
|
function subjectIsDirectory(toMatchCallee) {
|
|
2165
2024
|
const expectCall = toMatchCallee.object;
|
|
@@ -2184,7 +2043,7 @@ function subjectIsDirectory(toMatchCallee) {
|
|
|
2184
2043
|
const c6ToMatchExtension = {
|
|
2185
2044
|
create(context) {
|
|
2186
2045
|
const file = context.physicalFilename;
|
|
2187
|
-
if (!TEST_FILE$
|
|
2046
|
+
if (!TEST_FILE$10.test(file) || !segments(file).includes("specs")) return {};
|
|
2188
2047
|
return { CallExpression(node) {
|
|
2189
2048
|
const callee = node.callee;
|
|
2190
2049
|
if (callee === void 0 || memberPropertyName(callee) !== "toMatch") return;
|
|
@@ -2209,7 +2068,7 @@ const c6ToMatchExtension = {
|
|
|
2209
2068
|
};
|
|
2210
2069
|
//#endregion
|
|
2211
2070
|
//#region src/lint/rules/c7-seeds-sql-only.ts
|
|
2212
|
-
const TEST_FILE$
|
|
2071
|
+
const TEST_FILE$9 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2213
2072
|
/**
|
|
2214
2073
|
* CONVENTIONS C7 — `.seed()` carries DATABASE state only: `_seeds/` contains
|
|
2215
2074
|
* nothing but `*.sql` fragments. File state goes through `.fixture()`; there
|
|
@@ -2219,7 +2078,7 @@ const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/;
|
|
|
2219
2078
|
const c7SeedsSqlOnly = {
|
|
2220
2079
|
create(context) {
|
|
2221
2080
|
const file = context.physicalFilename;
|
|
2222
|
-
if (!TEST_FILE$
|
|
2081
|
+
if (!TEST_FILE$9.test(file) || !segments(file).includes("specs")) return {};
|
|
2223
2082
|
return { Program(node) {
|
|
2224
2083
|
const seedsDir = (0, node_path.join)((0, node_path.dirname)(file), GROUND_SEEDS);
|
|
2225
2084
|
for (const entry of listDirectory(seedsDir) ?? []) if (!entry.endsWith(".sql")) context.report({
|
|
@@ -2237,7 +2096,7 @@ const c7SeedsSqlOnly = {
|
|
|
2237
2096
|
};
|
|
2238
2097
|
//#endregion
|
|
2239
2098
|
//#region src/lint/rules/c8-referenced-fixture-exists.ts
|
|
2240
|
-
const TEST_FILE$
|
|
2099
|
+
const TEST_FILE$8 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2241
2100
|
/** The ground directory each fixture-referencing verb reads from. */
|
|
2242
2101
|
const VERB_ROOTS = {
|
|
2243
2102
|
request: GROUND_REQUESTS,
|
|
@@ -2270,7 +2129,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2270
2129
|
create(context) {
|
|
2271
2130
|
const file = context.physicalFilename;
|
|
2272
2131
|
const anchor = specsAnchor(file);
|
|
2273
|
-
if (!TEST_FILE$
|
|
2132
|
+
if (!TEST_FILE$8.test(file) || anchor === void 0) return {};
|
|
2274
2133
|
const featureDir = (0, node_path.dirname)(file);
|
|
2275
2134
|
return { CallExpression(node) {
|
|
2276
2135
|
const callee = node.callee;
|
|
@@ -2280,7 +2139,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2280
2139
|
if (verb === "fixture") {
|
|
2281
2140
|
const value = stringValue(args[0]);
|
|
2282
2141
|
if (value === void 0 || !looksLikePath(value)) return;
|
|
2283
|
-
const target = resolveFixture(value.replace(
|
|
2142
|
+
const target = resolveFixture(value.replace(/\/+$/u, ""), featureDir, anchor.directory);
|
|
2284
2143
|
if (target !== void 0 && !isFile(target) && !isDirectory(target)) context.report({
|
|
2285
2144
|
data: {
|
|
2286
2145
|
path: value,
|
|
@@ -2295,7 +2154,7 @@ const c8ReferencedFixtureExists = {
|
|
|
2295
2154
|
if (root === void 0) return;
|
|
2296
2155
|
const value = stringValue(args[0]);
|
|
2297
2156
|
if (value === void 0 || !looksLikePath(value)) return;
|
|
2298
|
-
const target = (0, node_path.join)(featureDir, root, value.replace(
|
|
2157
|
+
const target = (0, node_path.join)(featureDir, root, value.replace(/\/+$/u, ""));
|
|
2299
2158
|
if (!(verb === "toMatch" ? isFile(target) || isDirectory(target) : isFile(target))) context.report({
|
|
2300
2159
|
data: {
|
|
2301
2160
|
path: `${root}/${value}`,
|
|
@@ -2313,231 +2172,142 @@ const c8ReferencedFixtureExists = {
|
|
|
2313
2172
|
}
|
|
2314
2173
|
};
|
|
2315
2174
|
//#endregion
|
|
2316
|
-
//#region src/lint/rules/
|
|
2317
|
-
/** A
|
|
2318
|
-
const
|
|
2319
|
-
/** Is `node` the `x.response.body` member access itself? */
|
|
2320
|
-
function isResponseBody(node) {
|
|
2321
|
-
if (node.type !== "MemberExpression" || memberPropertyName(node) !== "body") return false;
|
|
2322
|
-
const object = node.object;
|
|
2323
|
-
return object !== void 0 && memberPropertyName(object) === "response";
|
|
2324
|
-
}
|
|
2325
|
-
/** Peel `await`, `as T`, and `!` off an initializer to reach its core expression. */
|
|
2326
|
-
function unwrap(node) {
|
|
2327
|
-
let current = node ?? void 0;
|
|
2328
|
-
while (current != null) if (current.type === "AwaitExpression") current = current.argument ?? void 0;
|
|
2329
|
-
else if (current.type === "TSAsExpression" || current.type === "TSNonNullExpression") current = current.expression ?? void 0;
|
|
2330
|
-
else if (current.type === "ParenthesizedExpression") current = current.expression ?? void 0;
|
|
2331
|
-
else return current;
|
|
2332
|
-
}
|
|
2175
|
+
//#region src/lint/rules/c10-contracts-boundary.ts
|
|
2176
|
+
/** A specifier reaching into a provider folder — the internal half of contracts/. */
|
|
2177
|
+
const INTERNAL_UNIT = /(?:^|\/)contracts\/(?:anthropic|http|openai)\//u;
|
|
2333
2178
|
/**
|
|
2334
|
-
* CONVENTIONS
|
|
2335
|
-
*
|
|
2336
|
-
*
|
|
2337
|
-
* `
|
|
2338
|
-
*
|
|
2339
|
-
*
|
|
2340
|
-
*
|
|
2341
|
-
* A probe is a body-rooted read: a direct `.response.body` use, or a field
|
|
2342
|
-
* access on the local binding a `const body = result.response.body as {…}`
|
|
2343
|
-
* introduces. The count is the same threshold the golden transformation applies.
|
|
2179
|
+
* CONVENTIONS C10 — the contracts boundary. A feature's `contracts/` folder has
|
|
2180
|
+
* a PUBLIC half (`*.contracts.ts` facades: the default export is the world, the
|
|
2181
|
+
* named exports are its scenarios) and an INTERNAL half (the `http/`, `openai/`
|
|
2182
|
+
* and `anthropic/` unit contracts the facades compose). Outside `contracts/`,
|
|
2183
|
+
* importing a unit is an error — a test routes through the facade, so a
|
|
2184
|
+
* scenario is named once, next to the world it derives from.
|
|
2344
2185
|
*/
|
|
2345
|
-
const
|
|
2186
|
+
const c10ContractsBoundary = {
|
|
2346
2187
|
create(context) {
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
let responseBodyNodes = 0;
|
|
2355
|
-
walk(callback, (inner) => {
|
|
2356
|
-
if (isResponseBody(inner)) responseBodyNodes++;
|
|
2357
|
-
if (inner.type === "VariableDeclarator") {
|
|
2358
|
-
const init = unwrap(inner.init);
|
|
2359
|
-
const id = inner.id;
|
|
2360
|
-
if (init !== void 0 && isResponseBody(init) && id?.type === "Identifier") aliases.add(id.name);
|
|
2361
|
-
}
|
|
2362
|
-
});
|
|
2363
|
-
let aliasReads = 0;
|
|
2364
|
-
walk(callback, (inner) => {
|
|
2365
|
-
if (inner.type !== "MemberExpression") return;
|
|
2366
|
-
const object = inner.object;
|
|
2367
|
-
if (object?.type === "Identifier" && aliases.has(object.name)) aliasReads++;
|
|
2368
|
-
});
|
|
2369
|
-
const probes = responseBodyNodes - aliases.size + aliasReads;
|
|
2370
|
-
if (probes >= threshold) context.report({
|
|
2371
|
-
data: { count: String(probes) },
|
|
2372
|
-
messageId: "bodyProbeCluster",
|
|
2188
|
+
const parts = segments(context.physicalFilename);
|
|
2189
|
+
if (!parts.includes("specs")) return {};
|
|
2190
|
+
if (parts.slice(0, -1).includes("contracts")) return {};
|
|
2191
|
+
return importSourceVisitor(({ node, source }) => {
|
|
2192
|
+
if (INTERNAL_UNIT.test(source)) context.report({
|
|
2193
|
+
data: { source },
|
|
2194
|
+
messageId: "internal",
|
|
2373
2195
|
node
|
|
2374
2196
|
});
|
|
2375
|
-
}
|
|
2197
|
+
});
|
|
2376
2198
|
},
|
|
2377
2199
|
meta: {
|
|
2378
|
-
docs: RULE_DOCS["
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
schema: [{
|
|
2382
|
-
additionalProperties: false,
|
|
2383
|
-
properties: { threshold: {
|
|
2384
|
-
minimum: 1,
|
|
2385
|
-
type: "integer"
|
|
2386
|
-
} },
|
|
2387
|
-
type: "object"
|
|
2388
|
-
}],
|
|
2389
|
-
type: "suggestion"
|
|
2200
|
+
docs: RULE_DOCS["c10-contracts-boundary"],
|
|
2201
|
+
messages: { internal: "Import \"{{source}}\" reaches into a provider folder — only contracts/*.contracts.ts is importable from a test; add a named scenario export there instead (C10 — see docs/13-linting.md)." },
|
|
2202
|
+
type: "problem"
|
|
2390
2203
|
}
|
|
2391
2204
|
};
|
|
2392
2205
|
//#endregion
|
|
2393
|
-
//#region src/lint/rules/
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
* negative assertion (`expect(() => …toMatch(…)).toThrow()` or
|
|
2407
|
-
* `expect(…toMatch(…)).rejects.toThrow()`)? The subject's own `expect(x)` is the
|
|
2408
|
-
* member OBJECT of `.toMatch` (a descendant), never an ancestor, so a positive
|
|
2409
|
-
* `expect(x).toMatch('f')` never trips this. The walk stays inside the current
|
|
2410
|
-
* expression: it stops at the first statement boundary or non-`expect` call, so
|
|
2411
|
-
* a `toMatch` merely written inside `inUpdateMode(() => …)` is NOT flagged.
|
|
2412
|
-
*/
|
|
2413
|
-
const TRANSPARENT_ANCESTORS = /* @__PURE__ */ new Set([
|
|
2414
|
-
"ArrowFunctionExpression",
|
|
2415
|
-
"AwaitExpression",
|
|
2416
|
-
"ChainExpression",
|
|
2417
|
-
"MemberExpression",
|
|
2418
|
-
"ParenthesizedExpression",
|
|
2419
|
-
"TSAsExpression",
|
|
2420
|
-
"TSNonNullExpression"
|
|
2421
|
-
]);
|
|
2422
|
-
function isWrappedInExpect(node) {
|
|
2423
|
-
let current = node.parent;
|
|
2424
|
-
while (current !== void 0) {
|
|
2425
|
-
if (current.type === "CallExpression") {
|
|
2426
|
-
const callee = current.callee;
|
|
2427
|
-
return callee?.type === "Identifier" && callee.name === "expect";
|
|
2428
|
-
}
|
|
2429
|
-
if (!TRANSPARENT_ANCESTORS.has(current.type)) return false;
|
|
2430
|
-
current = current.parent;
|
|
2431
|
-
}
|
|
2432
|
-
return false;
|
|
2206
|
+
//#region src/lint/rules/c11-contract-data-pairing.ts
|
|
2207
|
+
const TEST_FILE$7 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2208
|
+
/** The provider folders that may carry data files. */
|
|
2209
|
+
const PROVIDERS = [
|
|
2210
|
+
"anthropic",
|
|
2211
|
+
"http",
|
|
2212
|
+
"openai"
|
|
2213
|
+
];
|
|
2214
|
+
/** Data files: served payloads and matched inputs, both owned by a contract. */
|
|
2215
|
+
const DATA_SUFFIXES = [".request.ts", ".response.json"];
|
|
2216
|
+
/** The owning contract of a data file: its name up to the FIRST dot, `.ts`. */
|
|
2217
|
+
function ownerOf(entry) {
|
|
2218
|
+
return `${entry.slice(0, entry.indexOf("."))}.ts`;
|
|
2433
2219
|
}
|
|
2434
2220
|
/**
|
|
2435
|
-
* CONVENTIONS
|
|
2436
|
-
*
|
|
2437
|
-
*
|
|
2438
|
-
*
|
|
2439
|
-
*
|
|
2440
|
-
*
|
|
2441
|
-
* rewritten and the mismatch/error still throws in update mode.
|
|
2442
|
-
*
|
|
2443
|
-
* The structural signal is precise for the wrapped forms; a `toMatch` routed
|
|
2444
|
-
* through a helper that owns the try/catch (`catchMessage(() => …toMatch(…))`)
|
|
2445
|
-
* is out of static reach — see the D13 process note for that residue.
|
|
2221
|
+
* CONVENTIONS C11 — data is owned by a contract. Inside a provider folder every
|
|
2222
|
+
* `*.response.json` (served payload) and `*.request.ts` (matched prompt/body)
|
|
2223
|
+
* pairs with the sibling `<stem>.ts` that serves it, where the stem is the name
|
|
2224
|
+
* up to the FIRST dot: `events.fr.response.json` belongs to `events.ts`. A data
|
|
2225
|
+
* file with no owner is dead weight no test can reach — reported on the feature's
|
|
2226
|
+
* test file, since oxlint never visits a `.json`.
|
|
2446
2227
|
*/
|
|
2447
|
-
const
|
|
2228
|
+
const c11ContractDataPairing = {
|
|
2448
2229
|
create(context) {
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2230
|
+
const file = context.physicalFilename;
|
|
2231
|
+
if (!TEST_FILE$7.test(file) || !segments(file).includes("specs")) return {};
|
|
2232
|
+
return { Program(node) {
|
|
2233
|
+
const contractsDir = (0, node_path.join)((0, node_path.dirname)(file), "contracts");
|
|
2234
|
+
for (const provider of PROVIDERS) {
|
|
2235
|
+
const entries = listDirectory((0, node_path.join)(contractsDir, provider));
|
|
2236
|
+
if (entries === null) continue;
|
|
2237
|
+
const present = new Set(entries);
|
|
2238
|
+
for (const entry of entries) {
|
|
2239
|
+
if (!DATA_SUFFIXES.some((suffix) => entry.endsWith(suffix))) continue;
|
|
2240
|
+
const owner = ownerOf(entry);
|
|
2241
|
+
if (!present.has(owner)) context.report({
|
|
2242
|
+
data: {
|
|
2243
|
+
entry,
|
|
2244
|
+
owner,
|
|
2245
|
+
provider
|
|
2246
|
+
},
|
|
2247
|
+
messageId: "orphan",
|
|
2248
|
+
node
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2459
2252
|
} };
|
|
2460
2253
|
},
|
|
2461
2254
|
meta: {
|
|
2462
|
-
docs: RULE_DOCS["
|
|
2463
|
-
messages: {
|
|
2464
|
-
type: "
|
|
2255
|
+
docs: RULE_DOCS["c11-contract-data-pairing"],
|
|
2256
|
+
messages: { orphan: "contracts/{{provider}}/{{entry}} has no owning contract — data pairs with the sibling {{owner}} (stem = name up to the first dot) (C11 — see docs/13-linting.md)." },
|
|
2257
|
+
type: "problem"
|
|
2465
2258
|
}
|
|
2466
2259
|
};
|
|
2467
2260
|
//#endregion
|
|
2468
|
-
//#region src/lint/rules/
|
|
2469
|
-
|
|
2470
|
-
const MIN_STATUS = 100;
|
|
2471
|
-
const MAX_STATUS = 599;
|
|
2472
|
-
/** Matchers that pin a single value — the shape of a status probe. */
|
|
2473
|
-
const EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual"]);
|
|
2474
|
-
/** Is `node` an `expect(…)` call (bare `expect` identifier callee)? */
|
|
2475
|
-
function isExpectCall(node) {
|
|
2476
|
-
if (node?.type !== "CallExpression") return false;
|
|
2477
|
-
const callee = node.callee;
|
|
2478
|
-
return callee?.type === "Identifier" && callee.name === "expect";
|
|
2479
|
-
}
|
|
2480
|
-
/** Is `node` a numeric literal in the HTTP status range (100–599)? */
|
|
2481
|
-
function isStatusLiteral(node) {
|
|
2482
|
-
if (node?.type !== "Literal" || typeof node.value !== "number") return false;
|
|
2483
|
-
const value = node.value;
|
|
2484
|
-
return Number.isInteger(value) && value >= MIN_STATUS && value <= MAX_STATUS;
|
|
2485
|
-
}
|
|
2486
|
-
/**
|
|
2487
|
-
* Is `node` a status probe — `expect(X.status).toBe(N)` / `.toEqual(N)` where
|
|
2488
|
-
* `X.status` is any `.status` member access and `N` is a numeric literal in the
|
|
2489
|
-
* HTTP status range? The numeric-literal gate keeps container-status STRINGS
|
|
2490
|
-
* (`expect(neo.status).toBe('running')`) out.
|
|
2491
|
-
*/
|
|
2492
|
-
function isStatusProbe(node) {
|
|
2493
|
-
if (node.type !== "CallExpression") return false;
|
|
2494
|
-
const callee = node.callee;
|
|
2495
|
-
if (callee?.type !== "MemberExpression") return false;
|
|
2496
|
-
const matcher = memberPropertyName(callee);
|
|
2497
|
-
if (matcher === void 0 || !EQUALITY_MATCHERS.has(matcher)) return false;
|
|
2498
|
-
const expectCall = callee.object;
|
|
2499
|
-
if (!isExpectCall(expectCall)) return false;
|
|
2500
|
-
const subject = (expectCall?.arguments ?? [])[0];
|
|
2501
|
-
if (subject?.type !== "MemberExpression" || memberPropertyName(subject) !== "status") return false;
|
|
2502
|
-
const expected = (node.arguments ?? [])[0];
|
|
2503
|
-
return isStatusLiteral(expected);
|
|
2504
|
-
}
|
|
2261
|
+
//#region src/lint/rules/c13-underscored-ground.ts
|
|
2262
|
+
const TEST_FILE$6 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2505
2263
|
/**
|
|
2506
|
-
* CONVENTIONS
|
|
2507
|
-
*
|
|
2508
|
-
*
|
|
2509
|
-
* `
|
|
2264
|
+
* CONVENTIONS C13 — the ground of a spec carries a leading underscore. A
|
|
2265
|
+
* directory named `fixtures/`, `expected/`, `requests/` or `seeds/` under
|
|
2266
|
+
* `specs/` is a pre-14 name: since `@jterrazz/test` 14.0.0 the four are
|
|
2267
|
+
* `_fixtures/`, `_expected/`, `_requests/` and `_seeds/`, with no compatibility
|
|
2268
|
+
* fallback.
|
|
2510
2269
|
*
|
|
2511
|
-
*
|
|
2512
|
-
*
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
2515
|
-
*
|
|
2516
|
-
* the
|
|
2270
|
+
* The catch this rule exists for: a consumer that upgrades and keeps the old
|
|
2271
|
+
* names loses nothing loudly. Every resolver — `.fixture()`, `.seed()`,
|
|
2272
|
+
* `.request()`, `toMatch()` — looks under the underscored name, finds nothing,
|
|
2273
|
+
* and the tree becomes invisible rather than wrong. The failure that follows is
|
|
2274
|
+
* "fixture does not exist" on every spec at once, one step removed from the
|
|
2275
|
+
* cause; this rule states the cause and the rename.
|
|
2276
|
+
*
|
|
2277
|
+
* Anchored on the feature's visited test file, exactly as C2/C7/C11 are: the
|
|
2278
|
+
* spec's own sibling directories are probed, plus the `$FIXTURES` pool at the
|
|
2279
|
+
* nearest specs root. A DOMAIN folder named after the ground it holds
|
|
2280
|
+
* (`specs/api/requests/_requests/`) is never a probe target — only what sits
|
|
2281
|
+
* beside a spec is.
|
|
2517
2282
|
*/
|
|
2518
|
-
const
|
|
2283
|
+
const c13UnderscoredGround = {
|
|
2519
2284
|
create(context) {
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2285
|
+
const file = context.physicalFilename;
|
|
2286
|
+
const anchor = specsAnchor(file);
|
|
2287
|
+
if (!TEST_FILE$6.test(file) || anchor === void 0) return {};
|
|
2288
|
+
return { Program(node) {
|
|
2289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2290
|
+
const probe = (parent, legacy) => {
|
|
2291
|
+
const path = (0, node_path.join)(parent, legacy);
|
|
2292
|
+
if (seen.has(path) || !isDirectory(path)) return;
|
|
2293
|
+
seen.add(path);
|
|
2294
|
+
context.report({
|
|
2295
|
+
data: {
|
|
2296
|
+
ground: RENAMED_GROUND_DIRS[legacy] ?? legacy,
|
|
2297
|
+
legacy
|
|
2298
|
+
},
|
|
2299
|
+
messageId: "notUnderscored",
|
|
2300
|
+
node
|
|
2301
|
+
});
|
|
2302
|
+
};
|
|
2303
|
+
for (const legacy of Object.keys(RENAMED_GROUND_DIRS)) probe((0, node_path.dirname)(file), legacy);
|
|
2304
|
+
probe(anchor.directory, "fixtures");
|
|
2535
2305
|
} };
|
|
2536
2306
|
},
|
|
2537
2307
|
meta: {
|
|
2538
|
-
docs: RULE_DOCS["
|
|
2539
|
-
messages: {
|
|
2540
|
-
type: "
|
|
2308
|
+
docs: RULE_DOCS["c13-underscored-ground"],
|
|
2309
|
+
messages: { notUnderscored: "The directory \"{{legacy}}/\" is a pre-14 ground name — rename it to \"{{ground}}/\" (`git mv {{legacy}} {{ground}}`). What a spec stands on carries the underscore; a spec's own folder never does, and no resolver reads the un-underscored name (C13 — see docs/13-linting.md)." },
|
|
2310
|
+
type: "problem"
|
|
2541
2311
|
}
|
|
2542
2312
|
};
|
|
2543
2313
|
//#endregion
|
|
@@ -2673,7 +2443,7 @@ const KNOWN_TOKEN_LITERAL = new RegExp(`^\\{\\{(?:${[
|
|
|
2673
2443
|
"url",
|
|
2674
2444
|
"uuid",
|
|
2675
2445
|
"workdir"
|
|
2676
|
-
].join("|")})(?:#[\\w.-]+)?\\}\\}
|
|
2446
|
+
].join("|")})(?:#[\\w.-]+)?\\}\\}$`, "u");
|
|
2677
2447
|
/**
|
|
2678
2448
|
* Is this function a pure chain of `.replace(…, '{{token}}')` normalisations?
|
|
2679
2449
|
* Accepts an arrow with an expression body or a single-return body whose
|
|
@@ -2685,8 +2455,9 @@ function isTokenEquivalentTransform(fn) {
|
|
|
2685
2455
|
let expression = fn.body;
|
|
2686
2456
|
if (expression?.type === "BlockStatement") {
|
|
2687
2457
|
const statements = expression.body ?? [];
|
|
2688
|
-
|
|
2689
|
-
|
|
2458
|
+
const [only] = statements;
|
|
2459
|
+
if (statements.length !== 1 || only?.type !== "ReturnStatement") return false;
|
|
2460
|
+
expression = only.argument;
|
|
2690
2461
|
}
|
|
2691
2462
|
let sawReplace = false;
|
|
2692
2463
|
while (expression?.type === "CallExpression") {
|
|
@@ -2762,9 +2533,9 @@ const d8wTextBypass = {
|
|
|
2762
2533
|
};
|
|
2763
2534
|
//#endregion
|
|
2764
2535
|
//#region src/lint/rules/d9w-single-use-ref.ts
|
|
2765
|
-
const TEST_FILE$5 = /\.test\.[cm]?[jt]sx
|
|
2536
|
+
const TEST_FILE$5 = /\.test\.[cm]?[jt]sx?$/u;
|
|
2766
2537
|
/** A captured-ref token: `{{kind#ref}}`. Captures the ref name. */
|
|
2767
|
-
const REF_TOKEN = /\{\{[A-Za-z][A-Za-z0-9]*#(?<ref>[\w.-]+)\}\}/
|
|
2538
|
+
const REF_TOKEN = /\{\{[A-Za-z][A-Za-z0-9]*#(?<ref>[\w.-]+)\}\}/gu;
|
|
2768
2539
|
/** Is this call `match.ref(...)`? */
|
|
2769
2540
|
function isMatchRef(node) {
|
|
2770
2541
|
const callee = node.callee;
|
|
@@ -2819,7 +2590,7 @@ const d9wSingleUseRef = {
|
|
|
2819
2590
|
if (memberPropertyName(node.callee) === "toMatch") {
|
|
2820
2591
|
const name = stringValue(node.arguments?.[0]);
|
|
2821
2592
|
if (name !== void 0) {
|
|
2822
|
-
const target = (0, node_path.join)(featureDir, GROUND_EXPECTED, name.replace(
|
|
2593
|
+
const target = (0, node_path.join)(featureDir, GROUND_EXPECTED, name.replace(/\/+$/u, ""));
|
|
2823
2594
|
for (const ref of refsInFixture(target)) {
|
|
2824
2595
|
bump(ref);
|
|
2825
2596
|
if (!fixtureNodes.has(ref)) fixtureNodes.set(ref, node);
|
|
@@ -2844,6 +2615,234 @@ const d9wSingleUseRef = {
|
|
|
2844
2615
|
}
|
|
2845
2616
|
};
|
|
2846
2617
|
//#endregion
|
|
2618
|
+
//#region src/lint/rules/d12w-response-body-probe.ts
|
|
2619
|
+
/** A test reading the response body this many times (or more) is a cluster. */
|
|
2620
|
+
const DEFAULT_THRESHOLD = 3;
|
|
2621
|
+
/** Is `node` the `x.response.body` member access itself? */
|
|
2622
|
+
function isResponseBody(node) {
|
|
2623
|
+
if (node.type !== "MemberExpression" || memberPropertyName(node) !== "body") return false;
|
|
2624
|
+
const object = node.object;
|
|
2625
|
+
return object !== void 0 && memberPropertyName(object) === "response";
|
|
2626
|
+
}
|
|
2627
|
+
/** Peel `await`, `as T`, and `!` off an initializer to reach its core expression. */
|
|
2628
|
+
function unwrap(node) {
|
|
2629
|
+
let current = node ?? void 0;
|
|
2630
|
+
while (current != null) if (current.type === "AwaitExpression") current = current.argument ?? void 0;
|
|
2631
|
+
else if (current.type === "TSAsExpression" || current.type === "TSNonNullExpression") current = current.expression ?? void 0;
|
|
2632
|
+
else if (current.type === "ParenthesizedExpression") current = current.expression ?? void 0;
|
|
2633
|
+
else return current;
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* CONVENTIONS D12 (warning) — a full-response golden is the norm for API
|
|
2637
|
+
* assertions (the D11 boundary, mechanized). A test that accumulates a CLUSTER
|
|
2638
|
+
* of raw body probes — reading `.response.body` (or a variable cast from it)
|
|
2639
|
+
* `threshold`+ times in one test callback — should collapse into one golden:
|
|
2640
|
+
* `expect(result.response).toMatch('case.http')`. One or two probes stay silent
|
|
2641
|
+
* (a legitimate scalpel: absence, a single-field delta, third-party output).
|
|
2642
|
+
*
|
|
2643
|
+
* A probe is a body-rooted read: a direct `.response.body` use, or a field
|
|
2644
|
+
* access on the local binding a `const body = result.response.body as {…}`
|
|
2645
|
+
* introduces. The count is the same threshold the golden transformation applies.
|
|
2646
|
+
*/
|
|
2647
|
+
const d12wResponseBodyProbe = {
|
|
2648
|
+
create(context) {
|
|
2649
|
+
if (!segments(context.filename).includes("specs")) return {};
|
|
2650
|
+
const threshold = context.options[0]?.threshold ?? DEFAULT_THRESHOLD;
|
|
2651
|
+
return { CallExpression(node) {
|
|
2652
|
+
if (!isTestCallee(node.callee)) return;
|
|
2653
|
+
const callback = findTestCallback(node.arguments ?? []);
|
|
2654
|
+
if (callback === void 0) return;
|
|
2655
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
2656
|
+
let responseBodyNodes = 0;
|
|
2657
|
+
walk(callback, (inner) => {
|
|
2658
|
+
if (isResponseBody(inner)) responseBodyNodes++;
|
|
2659
|
+
if (inner.type === "VariableDeclarator") {
|
|
2660
|
+
const init = unwrap(inner.init);
|
|
2661
|
+
const id = inner.id;
|
|
2662
|
+
if (init !== void 0 && isResponseBody(init) && id?.type === "Identifier") aliases.add(id.name);
|
|
2663
|
+
}
|
|
2664
|
+
});
|
|
2665
|
+
let aliasReads = 0;
|
|
2666
|
+
walk(callback, (inner) => {
|
|
2667
|
+
if (inner.type !== "MemberExpression") return;
|
|
2668
|
+
const object = inner.object;
|
|
2669
|
+
if (object?.type === "Identifier" && aliases.has(object.name)) aliasReads++;
|
|
2670
|
+
});
|
|
2671
|
+
const probes = responseBodyNodes - aliases.size + aliasReads;
|
|
2672
|
+
if (probes >= threshold) context.report({
|
|
2673
|
+
data: { count: String(probes) },
|
|
2674
|
+
messageId: "bodyProbeCluster",
|
|
2675
|
+
node
|
|
2676
|
+
});
|
|
2677
|
+
} };
|
|
2678
|
+
},
|
|
2679
|
+
meta: {
|
|
2680
|
+
docs: RULE_DOCS["d12w-response-body-probe"],
|
|
2681
|
+
defaultOptions: [{ threshold: DEFAULT_THRESHOLD }],
|
|
2682
|
+
messages: { bodyProbeCluster: "{{count}} body probes in one test — this wants a full golden: expect(result.response).toMatch('x.http') (D12 — see docs/13-linting.md)." },
|
|
2683
|
+
schema: [{
|
|
2684
|
+
additionalProperties: false,
|
|
2685
|
+
properties: { threshold: {
|
|
2686
|
+
minimum: 1,
|
|
2687
|
+
type: "integer"
|
|
2688
|
+
} },
|
|
2689
|
+
type: "object"
|
|
2690
|
+
}],
|
|
2691
|
+
type: "suggestion"
|
|
2692
|
+
}
|
|
2693
|
+
};
|
|
2694
|
+
//#endregion
|
|
2695
|
+
//#region src/lint/rules/d13w-unfrozen-negative-fixture.ts
|
|
2696
|
+
/**
|
|
2697
|
+
* Does `arguments[1]` of a `toMatch(name, options)` call carry `{ frozen: true }`?
|
|
2698
|
+
* A frozen fixture opts out of update-mode rewriting, so it is exempt.
|
|
2699
|
+
*/
|
|
2700
|
+
function hasFrozenOption(args) {
|
|
2701
|
+
const options = args[1];
|
|
2702
|
+
if (options === void 0 || options.type !== "ObjectExpression") return false;
|
|
2703
|
+
const value = findProperty(options, "frozen")?.value;
|
|
2704
|
+
return value?.type === "Literal" && value.value === true;
|
|
2705
|
+
}
|
|
2706
|
+
/**
|
|
2707
|
+
* Is this `toMatch(…)` call nested inside a WRAPPING `expect(…)` — the shape of a
|
|
2708
|
+
* negative assertion (`expect(() => …toMatch(…)).toThrow()` or
|
|
2709
|
+
* `expect(…toMatch(…)).rejects.toThrow()`)? The subject's own `expect(x)` is the
|
|
2710
|
+
* member OBJECT of `.toMatch` (a descendant), never an ancestor, so a positive
|
|
2711
|
+
* `expect(x).toMatch('f')` never trips this. The walk stays inside the current
|
|
2712
|
+
* expression: it stops at the first statement boundary or non-`expect` call, so
|
|
2713
|
+
* a `toMatch` merely written inside `inUpdateMode(() => …)` is NOT flagged.
|
|
2714
|
+
*/
|
|
2715
|
+
const TRANSPARENT_ANCESTORS = /* @__PURE__ */ new Set([
|
|
2716
|
+
"ArrowFunctionExpression",
|
|
2717
|
+
"AwaitExpression",
|
|
2718
|
+
"ChainExpression",
|
|
2719
|
+
"MemberExpression",
|
|
2720
|
+
"ParenthesizedExpression",
|
|
2721
|
+
"TSAsExpression",
|
|
2722
|
+
"TSNonNullExpression"
|
|
2723
|
+
]);
|
|
2724
|
+
function isWrappedInExpect(node) {
|
|
2725
|
+
let current = node.parent;
|
|
2726
|
+
while (current !== void 0) {
|
|
2727
|
+
if (current.type === "CallExpression") {
|
|
2728
|
+
const callee = current.callee;
|
|
2729
|
+
return callee?.type === "Identifier" && callee.name === "expect";
|
|
2730
|
+
}
|
|
2731
|
+
if (!TRANSPARENT_ANCESTORS.has(current.type)) return false;
|
|
2732
|
+
current = current.parent;
|
|
2733
|
+
}
|
|
2734
|
+
return false;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* CONVENTIONS D13 (warning) — a `toMatch` whose failure is the behaviour under
|
|
2738
|
+
* test (wrapped in `expect(() => …).toThrow()` or `expect(…).rejects.toThrow()`)
|
|
2739
|
+
* asserts a DELIBERATELY-WRONG or MISSING fixture. Under `TEST_UPDATE=1` an
|
|
2740
|
+
* unfrozen fixture is silently rewritten with the actual output — the matcher
|
|
2741
|
+
* writes instead of throwing — which destroys the negative case (the assertion
|
|
2742
|
+
* then no longer throws). Pass `{ frozen: true }` so the fixture is never
|
|
2743
|
+
* rewritten and the mismatch/error still throws in update mode.
|
|
2744
|
+
*
|
|
2745
|
+
* The structural signal is precise for the wrapped forms; a `toMatch` routed
|
|
2746
|
+
* through a helper that owns the try/catch (`catchMessage(() => …toMatch(…))`)
|
|
2747
|
+
* is out of static reach — see the D13 process note for that residue.
|
|
2748
|
+
*/
|
|
2749
|
+
const d13wUnfrozenNegativeFixture = {
|
|
2750
|
+
create(context) {
|
|
2751
|
+
if (!segments(context.filename).includes("specs")) return {};
|
|
2752
|
+
return { CallExpression(node) {
|
|
2753
|
+
const callee = node.callee;
|
|
2754
|
+
if (callee === void 0 || memberPropertyName(callee) !== "toMatch") return;
|
|
2755
|
+
if (!isWrappedInExpect(node)) return;
|
|
2756
|
+
if (hasFrozenOption(node.arguments ?? [])) return;
|
|
2757
|
+
context.report({
|
|
2758
|
+
messageId: "unfrozenNegativeFixture",
|
|
2759
|
+
node
|
|
2760
|
+
});
|
|
2761
|
+
} };
|
|
2762
|
+
},
|
|
2763
|
+
meta: {
|
|
2764
|
+
docs: RULE_DOCS["d13w-unfrozen-negative-fixture"],
|
|
2765
|
+
messages: { unfrozenNegativeFixture: "This toMatch asserts a mismatch (wrapped in expect(…).toThrow/.rejects) — pass { frozen: true } so TEST_UPDATE=1 never overwrites the deliberately-wrong fixture (D13 — see docs/13-linting.md)." },
|
|
2766
|
+
type: "suggestion"
|
|
2767
|
+
}
|
|
2768
|
+
};
|
|
2769
|
+
//#endregion
|
|
2770
|
+
//#region src/lint/rules/d15w-status-only-probe.ts
|
|
2771
|
+
/** An HTTP status code lives in this range — the numeric-literal gate. */
|
|
2772
|
+
const MIN_STATUS = 100;
|
|
2773
|
+
const MAX_STATUS = 599;
|
|
2774
|
+
/** Matchers that pin a single value — the shape of a status probe. */
|
|
2775
|
+
const EQUALITY_MATCHERS = /* @__PURE__ */ new Set(["toBe", "toEqual"]);
|
|
2776
|
+
/** Is `node` an `expect(…)` call (bare `expect` identifier callee)? */
|
|
2777
|
+
function isExpectCall(node) {
|
|
2778
|
+
if (node?.type !== "CallExpression") return false;
|
|
2779
|
+
const callee = node.callee;
|
|
2780
|
+
return callee?.type === "Identifier" && callee.name === "expect";
|
|
2781
|
+
}
|
|
2782
|
+
/** Is `node` a numeric literal in the HTTP status range (100–599)? */
|
|
2783
|
+
function isStatusLiteral(node) {
|
|
2784
|
+
if (node?.type !== "Literal" || typeof node.value !== "number") return false;
|
|
2785
|
+
const { value } = node;
|
|
2786
|
+
return Number.isInteger(value) && value >= MIN_STATUS && value <= MAX_STATUS;
|
|
2787
|
+
}
|
|
2788
|
+
/**
|
|
2789
|
+
* Is `node` a status probe — `expect(X.status).toBe(N)` / `.toEqual(N)` where
|
|
2790
|
+
* `X.status` is any `.status` member access and `N` is a numeric literal in the
|
|
2791
|
+
* HTTP status range? The numeric-literal gate keeps container-status STRINGS
|
|
2792
|
+
* (`expect(neo.status).toBe('running')`) out.
|
|
2793
|
+
*/
|
|
2794
|
+
function isStatusProbe(node) {
|
|
2795
|
+
if (node.type !== "CallExpression") return false;
|
|
2796
|
+
const callee = node.callee;
|
|
2797
|
+
if (callee?.type !== "MemberExpression") return false;
|
|
2798
|
+
const matcher = memberPropertyName(callee);
|
|
2799
|
+
if (matcher === void 0 || !EQUALITY_MATCHERS.has(matcher)) return false;
|
|
2800
|
+
const expectCall = callee.object;
|
|
2801
|
+
if (!isExpectCall(expectCall)) return false;
|
|
2802
|
+
const subject = (expectCall?.arguments ?? [])[0];
|
|
2803
|
+
if (subject?.type !== "MemberExpression" || memberPropertyName(subject) !== "status") return false;
|
|
2804
|
+
const expected = (node.arguments ?? [])[0];
|
|
2805
|
+
return isStatusLiteral(expected);
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* CONVENTIONS D11/D15 (warning) — a spec test whose ONLY assertions are HTTP
|
|
2809
|
+
* status probes (`expect(X.status).toBe(N)` / `.toEqual(N)`, N a numeric literal
|
|
2810
|
+
* 100–599) throws away the full-response golden. Pin the whole response instead:
|
|
2811
|
+
* `expect(result.response).toMatch('case.http')`.
|
|
2812
|
+
*
|
|
2813
|
+
* This complements d12w (which needs a CLUSTER of ≥3 body probes): the lone
|
|
2814
|
+
* `expect(result.status).toBe(422)` as a test's sole assertion falls below that
|
|
2815
|
+
* threshold yet still hides everything the response carries. The rule fires only
|
|
2816
|
+
* when EVERY `expect(…)` in the callback is a status probe — a status probe
|
|
2817
|
+
* sitting NEXT TO a real assertion (a golden, `toMatchRows`, `toContain`, …) is
|
|
2818
|
+
* the legitimate scalpel and stays silent.
|
|
2819
|
+
*/
|
|
2820
|
+
const d15wStatusOnlyProbe = {
|
|
2821
|
+
create(context) {
|
|
2822
|
+
if (!segments(context.filename).includes("specs")) return {};
|
|
2823
|
+
return { CallExpression(node) {
|
|
2824
|
+
if (!isTestCallee(node.callee)) return;
|
|
2825
|
+
const callback = findTestCallback(node.arguments ?? []);
|
|
2826
|
+
if (callback === void 0) return;
|
|
2827
|
+
let expectCalls = 0;
|
|
2828
|
+
let statusProbes = 0;
|
|
2829
|
+
walk(callback, (inner) => {
|
|
2830
|
+
if (isExpectCall(inner)) expectCalls++;
|
|
2831
|
+
if (isStatusProbe(inner)) statusProbes++;
|
|
2832
|
+
});
|
|
2833
|
+
if (expectCalls >= 1 && statusProbes === expectCalls) context.report({
|
|
2834
|
+
messageId: "statusOnlyProbe",
|
|
2835
|
+
node
|
|
2836
|
+
});
|
|
2837
|
+
} };
|
|
2838
|
+
},
|
|
2839
|
+
meta: {
|
|
2840
|
+
docs: RULE_DOCS["d15w-status-only-probe"],
|
|
2841
|
+
messages: { statusOnlyProbe: "The test's only assertion(s) are status probes — pin the full response: expect(result.response).toMatch('case.http') (D11 — see docs/13-linting.md)." },
|
|
2842
|
+
type: "suggestion"
|
|
2843
|
+
}
|
|
2844
|
+
};
|
|
2845
|
+
//#endregion
|
|
2847
2846
|
//#region src/lint/package-exports.ts
|
|
2848
2847
|
/**
|
|
2849
2848
|
* The subpaths this package actually publishes, read from its OWN
|
|
@@ -2897,7 +2896,7 @@ function findOwnManifest(startDir) {
|
|
|
2897
2896
|
*/
|
|
2898
2897
|
function declaredSubpaths(startDir) {
|
|
2899
2898
|
if (startDir === void 0 && cached !== null) return cached;
|
|
2900
|
-
const exports = findOwnManifest(startDir ??
|
|
2899
|
+
const exports = findOwnManifest(startDir ?? __dirname)?.exports;
|
|
2901
2900
|
const subpaths = exports !== null && typeof exports === "object" ? Object.keys(exports).filter((key) => key.startsWith("./") && key !== ".").map((key) => `${PACKAGE$2}/${key.slice(2)}`).sort() : [];
|
|
2902
2901
|
if (startDir === void 0) cached = subpaths;
|
|
2903
2902
|
return subpaths;
|
|
@@ -2939,10 +2938,10 @@ const f1NoSubpathImport = {
|
|
|
2939
2938
|
};
|
|
2940
2939
|
//#endregion
|
|
2941
2940
|
//#region src/lint/rules/f2-no-test-imports-in-prod.ts
|
|
2942
|
-
const TEST_FILE$4 = /\.(?:test|fixtures|specification)\.[cm]?[jt]sx
|
|
2943
|
-
const CONFIG_FILE = /(?:^|[/\\])[\w.-]*\.config\.[cm]?[jt]s
|
|
2944
|
-
const TEST_IMPORT$1 = /\.(?:test|fixtures)(?:\.[cm]?[jt]sx?)
|
|
2945
|
-
const TOOL_SUBPATH
|
|
2941
|
+
const TEST_FILE$4 = /\.(?:test|fixtures|specification)\.[cm]?[jt]sx?$/u;
|
|
2942
|
+
const CONFIG_FILE = /(?:^|[/\\])[\w.-]*\.config\.[cm]?[jt]s$/u;
|
|
2943
|
+
const TEST_IMPORT$1 = /\.(?:test|fixtures)(?:\.[cm]?[jt]sx?)?$/u;
|
|
2944
|
+
const TOOL_SUBPATH = "@jterrazz/test/oxlint";
|
|
2946
2945
|
/**
|
|
2947
2946
|
* CONVENTIONS F2 — production code never imports a test artefact. In a
|
|
2948
2947
|
* non-test file (not `*.test.ts` / `*.fixtures.ts` / `*.specification.ts`,
|
|
@@ -2961,7 +2960,7 @@ const f2NoTestImportsInProd = {
|
|
|
2961
2960
|
const file = context.filename;
|
|
2962
2961
|
if (TEST_FILE$4.test(file) || CONFIG_FILE.test(file) || isUnderSpecs(file)) return {};
|
|
2963
2962
|
return { ...importSourceVisitor(({ node, source }) => {
|
|
2964
|
-
if (source === TOOL_SUBPATH
|
|
2963
|
+
if (source === TOOL_SUBPATH) return;
|
|
2965
2964
|
if (source === "vitest" || source.startsWith("vitest/")) context.report({
|
|
2966
2965
|
data: { source },
|
|
2967
2966
|
messageId: "testImport",
|
|
@@ -2988,7 +2987,6 @@ const f2NoTestImportsInProd = {
|
|
|
2988
2987
|
//#endregion
|
|
2989
2988
|
//#region src/lint/rules/f3-specs-public-entry.ts
|
|
2990
2989
|
const PACKAGE = "@jterrazz/test";
|
|
2991
|
-
const TOOL_SUBPATH = "@jterrazz/test/oxlint";
|
|
2992
2990
|
/**
|
|
2993
2991
|
* The framework's own top-level `src/` layers. Deep-importing any of these from
|
|
2994
2992
|
* a spec couples the spec to internals the public entry exists to hide. These
|
|
@@ -2998,20 +2996,27 @@ const TOOL_SUBPATH = "@jterrazz/test/oxlint";
|
|
|
2998
2996
|
* `server: () => createApp()` importing `../../src/app.js`).
|
|
2999
2997
|
*/
|
|
3000
2998
|
const FRAMEWORK_LAYERS = /* @__PURE__ */ new Set([
|
|
3001
|
-
"core",
|
|
3002
2999
|
"integrations",
|
|
3003
3000
|
"lint",
|
|
3001
|
+
"specification",
|
|
3004
3002
|
"vitest"
|
|
3005
3003
|
]);
|
|
3006
3004
|
/**
|
|
3007
3005
|
* CONVENTIONS F3 — from `specs/`, deep-importing the FRAMEWORK's internals is
|
|
3008
3006
|
* forbidden: a relative path resolving inside the framework repo's
|
|
3009
|
-
* `src/{
|
|
3010
|
-
*
|
|
3011
|
-
*
|
|
3007
|
+
* `src/{specification,integrations,vitest,lint}/`, or a `@jterrazz/test/<subpath>`
|
|
3008
|
+
* the package does not publish. A consumer's imports of its OWN app source are
|
|
3009
|
+
* always allowed — that is the pattern.
|
|
3010
|
+
*
|
|
3011
|
+
* The exempt subpaths are not listed here: they are read from the package's own
|
|
3012
|
+
* `exports` map, the same source F1 uses. A published subpath IS the public
|
|
3013
|
+
* surface — reaching `@jterrazz/test/vitest` from a specs-tree `vitest.config.ts`
|
|
3014
|
+
* is the prescribed gesture, not a leak — so what a spec must not reach is what
|
|
3015
|
+
* the manifest never published. The root entry (`.`) is not a subpath and never
|
|
3016
|
+
* concerned this branch: `@jterrazz/test` is the public entry F3 points at.
|
|
3012
3017
|
*
|
|
3013
|
-
* Distinct from F1 (which
|
|
3014
|
-
*
|
|
3018
|
+
* Distinct from F1 (which judges the same specifiers from any file): F3 is the
|
|
3019
|
+
* specs-specific guard that also reaches relative framework-internal paths.
|
|
3015
3020
|
* Documented exception: specs under `specs/integrations/` may deep-import the
|
|
3016
3021
|
* integration module they cover (`src/integrations/**`).
|
|
3017
3022
|
*/
|
|
@@ -3019,12 +3024,17 @@ const f3SpecsPublicEntry = {
|
|
|
3019
3024
|
create(context) {
|
|
3020
3025
|
const anchor = specsAnchor(context.filename);
|
|
3021
3026
|
if (anchor === void 0) return {};
|
|
3027
|
+
const published = new Set(declaredSubpaths());
|
|
3028
|
+
const exempt = published.size === 0 ? "none" : [...published].join(", ");
|
|
3022
3029
|
const underIntegrations = anchor.relative[0] === "integrations";
|
|
3023
3030
|
return { ...importSourceVisitor(({ node, source }) => {
|
|
3024
3031
|
if (source.startsWith(`${PACKAGE}/`)) {
|
|
3025
|
-
if (source
|
|
3032
|
+
if (published.has(source)) return;
|
|
3026
3033
|
context.report({
|
|
3027
|
-
data: {
|
|
3034
|
+
data: {
|
|
3035
|
+
published: exempt,
|
|
3036
|
+
source
|
|
3037
|
+
},
|
|
3028
3038
|
messageId: "deepImport",
|
|
3029
3039
|
node
|
|
3030
3040
|
});
|
|
@@ -3032,11 +3042,14 @@ const f3SpecsPublicEntry = {
|
|
|
3032
3042
|
}
|
|
3033
3043
|
const marker = source.indexOf("/src/");
|
|
3034
3044
|
if (marker === -1 && !source.startsWith("src/")) return;
|
|
3035
|
-
const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0];
|
|
3045
|
+
const layer = (marker === -1 ? source.slice(4) : source.slice(marker + 5)).split("/")[0] ?? "";
|
|
3036
3046
|
if (!FRAMEWORK_LAYERS.has(layer)) return;
|
|
3037
3047
|
if (underIntegrations && layer === "integrations") return;
|
|
3038
3048
|
context.report({
|
|
3039
|
-
data: {
|
|
3049
|
+
data: {
|
|
3050
|
+
published: exempt,
|
|
3051
|
+
source
|
|
3052
|
+
},
|
|
3040
3053
|
messageId: "deepImport",
|
|
3041
3054
|
node
|
|
3042
3055
|
});
|
|
@@ -3044,14 +3057,14 @@ const f3SpecsPublicEntry = {
|
|
|
3044
3057
|
},
|
|
3045
3058
|
meta: {
|
|
3046
3059
|
docs: RULE_DOCS["f3-specs-public-entry"],
|
|
3047
|
-
messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md
|
|
3060
|
+
messages: { deepImport: "specs/ must not deep-import framework internals — reach the framework via its public entry (@jterrazz/test, or src/index.js in this repo), not \"{{source}}\" (F3 — see docs/13-linting.md). Exempt: the published subpaths {{published}}, and src/integrations/** from specs/integrations/." },
|
|
3048
3061
|
type: "problem"
|
|
3049
3062
|
}
|
|
3050
3063
|
};
|
|
3051
3064
|
//#endregion
|
|
3052
3065
|
//#region src/lint/rules/f4-no-test-to-test-import.ts
|
|
3053
|
-
const TEST_FILE$3 = /\.test\.[cm]?[jt]sx
|
|
3054
|
-
const TEST_IMPORT = /\.test(?:\.[cm]?[jt]sx?)
|
|
3066
|
+
const TEST_FILE$3 = /\.test\.[cm]?[jt]sx?$/u;
|
|
3067
|
+
const TEST_IMPORT = /\.test(?:\.[cm]?[jt]sx?)?$/u;
|
|
3055
3068
|
/**
|
|
3056
3069
|
* CONVENTIONS F4 — a `*.test.ts` never imports another `*.test.ts`. Shared
|
|
3057
3070
|
* test data belongs in a `*.fixtures.ts` neighbour (F5), shared runners in a
|
|
@@ -3076,8 +3089,8 @@ const f4NoTestToTestImport = {
|
|
|
3076
3089
|
};
|
|
3077
3090
|
//#endregion
|
|
3078
3091
|
//#region src/lint/rules/f5-fixtures-only-from-tests.ts
|
|
3079
|
-
const TEST_FILE$2 = /\.test\.[cm]?[jt]sx
|
|
3080
|
-
const FIXTURES_IMPORT = /\.fixtures(?:\.[cm]?[jt]sx?)
|
|
3092
|
+
const TEST_FILE$2 = /\.test\.[cm]?[jt]sx?$/u;
|
|
3093
|
+
const FIXTURES_IMPORT = /\.fixtures(?:\.[cm]?[jt]sx?)?$/u;
|
|
3081
3094
|
/**
|
|
3082
3095
|
* CONVENTIONS F5 — a `*.fixtures.ts` module is only importable from
|
|
3083
3096
|
* `*.test.ts` files. Any other importer (prod code, a specification file,
|
|
@@ -3102,7 +3115,7 @@ const f5FixturesOnlyFromTests = {
|
|
|
3102
3115
|
};
|
|
3103
3116
|
//#endregion
|
|
3104
3117
|
//#region src/lint/rules/i1-layer-boundaries.ts
|
|
3105
|
-
const TEST_OR_FIXTURES = /\.(?:test|fixtures)\.[cm]?[jt]sx
|
|
3118
|
+
const TEST_OR_FIXTURES = /\.(?:test|fixtures)\.[cm]?[jt]sx?$/u;
|
|
3106
3119
|
function matchesPackage(source, packages) {
|
|
3107
3120
|
return packages.some((name) => source === name || source.startsWith(`${name}/`));
|
|
3108
3121
|
}
|
|
@@ -3114,7 +3127,7 @@ function pathInsideSrc(path) {
|
|
|
3114
3127
|
}
|
|
3115
3128
|
/** Strip a `.js`/`.ts`-style extension for exact-path comparison. */
|
|
3116
3129
|
function withoutExtension(path) {
|
|
3117
|
-
return path.replace(/\.[cm]?[jt]sx
|
|
3130
|
+
return path.replace(/\.[cm]?[jt]sx?$/u, "");
|
|
3118
3131
|
}
|
|
3119
3132
|
/** Does `target` match one of the declared paths (prefix, or exact module)? */
|
|
3120
3133
|
function matchesPath(target, patterns) {
|
|
@@ -3231,7 +3244,7 @@ const i1LayerBoundaries = {
|
|
|
3231
3244
|
};
|
|
3232
3245
|
//#endregion
|
|
3233
3246
|
//#region src/lint/rules/i2-sibling-test-naming.ts
|
|
3234
|
-
const TEST_FILE$1 = /\.test\.[cm]?[jt]sx
|
|
3247
|
+
const TEST_FILE$1 = /\.test\.[cm]?[jt]sx?$/u;
|
|
3235
3248
|
/**
|
|
3236
3249
|
* CONVENTIONS I2 — module tests are NEIGHBOURS: the test of `<file>.ts` is
|
|
3237
3250
|
* `<file>.test.ts` next to it (Go's `foo_test.go` parity). Flags, on visited
|
|
@@ -3259,7 +3272,7 @@ const i2SiblingTestNaming = {
|
|
|
3259
3272
|
".js"
|
|
3260
3273
|
].map((extension) => file.replace(TEST_FILE$1, extension));
|
|
3261
3274
|
if (!neighbours.some(isFile)) context.report({
|
|
3262
|
-
data: { expected: neighbours[0]
|
|
3275
|
+
data: { expected: neighbours[0]?.split(/[/\\]/u).at(-1) ?? "" },
|
|
3263
3276
|
messageId: "orphanTest",
|
|
3264
3277
|
node
|
|
3265
3278
|
});
|
|
@@ -3286,7 +3299,7 @@ const i2SiblingTestNaming = {
|
|
|
3286
3299
|
};
|
|
3287
3300
|
//#endregion
|
|
3288
3301
|
//#region src/lint/rules/i4-no-vi-mock-in-src.ts
|
|
3289
|
-
const TEST_FILE = /\.test\.[cm]?[jt]sx
|
|
3302
|
+
const TEST_FILE = /\.test\.[cm]?[jt]sx?$/u;
|
|
3290
3303
|
const DATA_ASSET = new RegExp(`\\.(?:${[
|
|
3291
3304
|
"avif",
|
|
3292
3305
|
"bin",
|
|
@@ -3324,10 +3337,10 @@ const DATA_ASSET = new RegExp(`\\.(?:${[
|
|
|
3324
3337
|
"yaml",
|
|
3325
3338
|
"yml",
|
|
3326
3339
|
"zip"
|
|
3327
|
-
].join("|")})$`, "
|
|
3340
|
+
].join("|")})$`, "iu");
|
|
3328
3341
|
/** A bundler suffix (`./payload.json?raw`, `./doc.md#frag`) is not part of the extension. */
|
|
3329
3342
|
function withoutSuffix(source) {
|
|
3330
|
-
return source.replace(/[?#]
|
|
3343
|
+
return source.replace(/[?#].*$/u, "");
|
|
3331
3344
|
}
|
|
3332
3345
|
/**
|
|
3333
3346
|
* CONVENTIONS I4 — in module tests under `src/`, mocks and data are CODE:
|
|
@@ -3567,7 +3580,7 @@ const plugin = {
|
|
|
3567
3580
|
*
|
|
3568
3581
|
* 'jterrazz/b5-await-using': ['error', { runners: ['dockerCli'] }]
|
|
3569
3582
|
*/
|
|
3570
|
-
const recommendedRules = Object.fromEntries(Object.keys(plugin.rules).map((rule) => [`jterrazz/${rule}`, /^\w+w
|
|
3583
|
+
const recommendedRules = Object.fromEntries(Object.keys(plugin.rules).map((rule) => [`jterrazz/${rule}`, /^\w+w-/u.test(rule) ? "warn" : "error"]));
|
|
3571
3584
|
/**
|
|
3572
3585
|
* The composable testing fragment — wire the plugin, enable the whole
|
|
3573
3586
|
* catalogue, and ship the one override every strict consumer needs. Designed
|