oxlint-plugin-arch 0.5.0 → 0.6.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.
@@ -1211,7 +1211,13 @@ import { defineRule as defineRule5 } from "@oxlint/plugins";
1211
1211
  var testModifiers = defineRule5({
1212
1212
  meta: {
1213
1213
  type: "problem",
1214
- schema: [],
1214
+ schema: [
1215
+ {
1216
+ type: "object",
1217
+ additionalProperties: false,
1218
+ properties: { additionalTestFunctions: { type: "array", items: { type: "string" } } }
1219
+ }
1220
+ ],
1215
1221
  messages: {
1216
1222
  modifier: "Focused, skipped and placeholder tests are not allowed."
1217
1223
  }
@@ -1220,7 +1226,13 @@ var testModifiers = defineRule5({
1220
1226
  let names = new Set;
1221
1227
  return {
1222
1228
  before() {
1223
- names = new Set(["test", "it", "describe", "suite"]);
1229
+ names = new Set([
1230
+ "test",
1231
+ "it",
1232
+ "describe",
1233
+ "suite",
1234
+ ...optionsFirst(context, {}).additionalTestFunctions ?? []
1235
+ ]);
1224
1236
  },
1225
1237
  ImportDeclaration(node) {
1226
1238
  if (!["vitest", "bun:test", "@playwright/test", "@jest/globals"].includes(node.source.value)) {
@@ -1242,7 +1254,7 @@ var testModifiers = defineRule5({
1242
1254
  return;
1243
1255
  }
1244
1256
  const parts = name.split(".");
1245
- if (names.has(parts[0]) && ["only", "skip", "todo"].includes(parts.at(-1) ?? "")) {
1257
+ if ([...names].some((test) => name.startsWith(test + ".")) && ["only", "skip", "todo"].includes(parts.at(-1) ?? "")) {
1246
1258
  context.report({ node, messageId: "modifier" });
1247
1259
  }
1248
1260
  }
@@ -1250,9 +1262,306 @@ var testModifiers = defineRule5({
1250
1262
  }
1251
1263
  });
1252
1264
 
1253
- // src/rules/module-policy/reasoned-directives.ts
1265
+ // src/rules/module-policy/test-assertions.ts
1254
1266
  import { defineRule as defineRule6 } from "@oxlint/plugins";
1255
- var directives = defineRule6({
1267
+
1268
+ // src/constants/test.constants.ts
1269
+ var testAssertionFunctions = [
1270
+ "expect",
1271
+ "expect.poll",
1272
+ "expect.soft",
1273
+ "expect.assert",
1274
+ "expect.assertions",
1275
+ "expect.hasAssertions",
1276
+ "expectTypeOf",
1277
+ "assert",
1278
+ "assertType"
1279
+ ];
1280
+
1281
+ // src/utils/helpers/test-tables.ts
1282
+ function testTablesRegistration(call, names, functions) {
1283
+ let callee = call.callee;
1284
+ let table;
1285
+ if (callee.type === "CallExpression") {
1286
+ const builder = astDottedName(callee.callee);
1287
+ if (builder?.endsWith(".for") || builder?.endsWith(".each")) {
1288
+ const argument = callee.arguments[0];
1289
+ if (argument && argument.type !== "SpreadElement")
1290
+ table = argument;
1291
+ }
1292
+ callee = callee.callee;
1293
+ }
1294
+ if (callee.type === "TaggedTemplateExpression")
1295
+ callee = callee.tag;
1296
+ const name = astDottedName(callee);
1297
+ if (!name || !names.some((test) => name === test || name.startsWith(test + ".") && name.slice(test.length + 1).split(".").every((part) => ["for", "each", "concurrent", "sequential", "only", "skip"].includes(part))))
1298
+ return;
1299
+ const argument = call.arguments.at(-1);
1300
+ const callback = argument?.type === "Identifier" ? functions.get(argument) : argument;
1301
+ if (callback?.type === "FunctionDeclaration" || callback?.type === "FunctionExpression" || callback?.type === "ArrowFunctionExpression") {
1302
+ return { call, callback, table };
1303
+ }
1304
+ }
1305
+ function testTablesResolve(expression, values) {
1306
+ const seen = new Set;
1307
+ let value = trivialUnwrap(expression);
1308
+ while (values.has(value) && !seen.has(value)) {
1309
+ seen.add(value);
1310
+ value = trivialUnwrap(values.get(value));
1311
+ }
1312
+ return value;
1313
+ }
1314
+ function testTablesRows(registration, sourceCode, values, functions) {
1315
+ if (!registration.table)
1316
+ return;
1317
+ const table = testTablesResolve(registration.table, values);
1318
+ if (table.type !== "ArrayExpression")
1319
+ return;
1320
+ const parameter = registration.callback.params[0];
1321
+ const variables = sourceCode.getDeclaredVariables(registration.callback);
1322
+ return table.elements.map((element) => {
1323
+ const row = { bindings: new Map, callbacks: [] };
1324
+ if (!element || element.type === "SpreadElement")
1325
+ return row;
1326
+ const value = testTablesResolve(element, values);
1327
+ if (value.type !== "ObjectExpression" || value.properties.some((property) => property.type === "SpreadElement" || property.computed))
1328
+ return row;
1329
+ const properties = new Map;
1330
+ for (const property of value.properties) {
1331
+ if (property.type !== "Property" || property.kind !== "init")
1332
+ continue;
1333
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : undefined;
1334
+ if (!key)
1335
+ continue;
1336
+ properties.delete(key);
1337
+ const expression = property.value;
1338
+ const fn = functions.get(expression) ?? expression;
1339
+ if (fn.type === "FunctionDeclaration" || fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression")
1340
+ properties.set(key, fn);
1341
+ }
1342
+ row.callbacks.push(...properties.values());
1343
+ if (parameter?.type === "ObjectPattern") {
1344
+ for (const property of parameter.properties) {
1345
+ if (property.type !== "Property" || property.computed || property.value.type !== "Identifier")
1346
+ continue;
1347
+ const key = property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : "";
1348
+ const fn = properties.get(key);
1349
+ if (!fn)
1350
+ continue;
1351
+ const variable = variables.find((entry) => entry.identifiers.some((identifier) => identifier === property.value));
1352
+ for (const reference of variable?.references ?? [])
1353
+ row.bindings.set(reference.identifier, fn);
1354
+ }
1355
+ } else if (parameter?.type === "Identifier") {
1356
+ const variable = variables.find((entry) => entry.identifiers.includes(parameter));
1357
+ for (const reference of variable?.references ?? []) {
1358
+ const member = reference.identifier.parent;
1359
+ if (member.type !== "MemberExpression" || member.object !== reference.identifier || member.computed || member.property.type !== "Identifier")
1360
+ continue;
1361
+ const fn = properties.get(member.property.name);
1362
+ if (fn)
1363
+ row.bindings.set(member, fn);
1364
+ }
1365
+ }
1366
+ return row;
1367
+ });
1368
+ }
1369
+ function testTablesCallName(node) {
1370
+ if (node.type === "CallExpression")
1371
+ return testTablesCallName(node.callee);
1372
+ if (node.type === "Identifier")
1373
+ return node.name;
1374
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier")
1375
+ return;
1376
+ const parent = testTablesCallName(node.object);
1377
+ return parent ? `${parent}.${node.property.name}` : undefined;
1378
+ }
1379
+ function testTablesAssertionPattern(name) {
1380
+ const parts = name.split(".");
1381
+ const pattern = parts.map((part, index) => {
1382
+ if (part === "**")
1383
+ return "(?:\\.[^.]+)*";
1384
+ return (index ? "\\." : "") + part.replaceAll("$", "\\$").replaceAll("*", "[^.]*");
1385
+ }).join("");
1386
+ return new RegExp(`^${pattern}$`);
1387
+ }
1388
+
1389
+ // src/rules/module-policy/test-assertions.ts
1390
+ var testAssertions = defineRule6({
1391
+ meta: {
1392
+ type: "problem",
1393
+ schema: [
1394
+ {
1395
+ type: "object",
1396
+ additionalProperties: false,
1397
+ properties: {
1398
+ additionalTestFunctions: { type: "array", items: { type: "string" } },
1399
+ additionalAssertionFunctions: { type: "array", items: { type: "string" } }
1400
+ }
1401
+ }
1402
+ ],
1403
+ messages: {
1404
+ missing: "This test has no recognized assertion in every case. Assert the result directly or call its assertion helper. Register imported assertion helpers only after verifying that they fail on incorrect results.",
1405
+ standalone: "Put this assertion inside a test or an assertion helper. Table-row assertion callbacks must belong to a registered test."
1406
+ }
1407
+ },
1408
+ createOnce(context) {
1409
+ return {
1410
+ "Program:exit"(program) {
1411
+ const options = optionsFirst(context, {});
1412
+ const tests = ["test", "it", ...options.additionalTestFunctions ?? []];
1413
+ const functions = new Map;
1414
+ const values = new Map;
1415
+ const patterns = [
1416
+ ...testAssertionFunctions,
1417
+ "assert.*",
1418
+ ...options.additionalAssertionFunctions ?? []
1419
+ ].map(testTablesAssertionPattern);
1420
+ const aliases = new Map;
1421
+ const shadowed = new Set;
1422
+ const calls = [];
1423
+ const allowed = new Set;
1424
+ const assertionRoots = new Set([...testAssertionFunctions, ...options.additionalAssertionFunctions ?? []].map((name) => name.split(".")[0]));
1425
+ astVisit(program, [], (node) => {
1426
+ if (node.type === "CallExpression")
1427
+ calls.push(node);
1428
+ if (![
1429
+ "VariableDeclarator",
1430
+ "FunctionDeclaration",
1431
+ "FunctionExpression",
1432
+ "ArrowFunctionExpression",
1433
+ "ImportDeclaration"
1434
+ ].includes(node.type))
1435
+ return;
1436
+ for (const variable of context.sourceCode.getDeclaredVariables(node)) {
1437
+ const native = node.type === "ImportDeclaration" && ["vitest", "@jest/globals", "@playwright/test"].includes(node.source.value);
1438
+ if (assertionRoots.has(variable.name) && node.type !== "ImportDeclaration") {
1439
+ for (const reference of variable.references)
1440
+ shadowed.add(reference.identifier);
1441
+ }
1442
+ if (native && node.type === "ImportDeclaration") {
1443
+ const specifier = node.specifiers.find((entry) => entry.local.name === variable.name);
1444
+ if (specifier?.type === "ImportSpecifier") {
1445
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
1446
+ if (["test", "it"].includes(imported))
1447
+ tests.push(variable.name);
1448
+ if (assertionRoots.has(imported)) {
1449
+ for (const reference of variable.references)
1450
+ aliases.set(reference.identifier, imported);
1451
+ }
1452
+ }
1453
+ }
1454
+ const fn = node.type === "FunctionDeclaration" ? node : node.type === "VariableDeclarator" ? node.init : undefined;
1455
+ if (fn?.type === "FunctionDeclaration" || fn?.type === "FunctionExpression" || fn?.type === "ArrowFunctionExpression") {
1456
+ for (const reference of variable.references)
1457
+ functions.set(reference.identifier, fn);
1458
+ allowed.add(fn);
1459
+ }
1460
+ if (node.type === "VariableDeclarator" && node.init && node.parent.type === "VariableDeclaration" && node.parent.kind === "const") {
1461
+ for (const reference of variable.references)
1462
+ values.set(reference.identifier, node.init);
1463
+ }
1464
+ }
1465
+ });
1466
+ function assertionName(call) {
1467
+ let root = call.callee;
1468
+ while (root.type === "MemberExpression" || root.type === "CallExpression")
1469
+ root = root.type === "MemberExpression" ? root.object : root.callee;
1470
+ if (shadowed.has(root))
1471
+ return;
1472
+ const name = testTablesCallName(call.callee);
1473
+ const alias = aliases.get(root);
1474
+ return alias && name && root.type === "Identifier" ? alias + name.slice(root.name.length) : name;
1475
+ }
1476
+ function hasAssertion(fn, bindings, seen) {
1477
+ if (seen.has(fn))
1478
+ return false;
1479
+ seen.add(fn);
1480
+ let found = false;
1481
+ astVisit(fn.body, [], (node, ancestors) => {
1482
+ if (found || node.type !== "CallExpression")
1483
+ return;
1484
+ if (ancestors.some((ancestor) => ancestor.type === "FunctionDeclaration" || (ancestor.type === "FunctionExpression" || ancestor.type === "ArrowFunctionExpression") && ancestor.parent.type !== "CallExpression"))
1485
+ return;
1486
+ const name = assertionName(node);
1487
+ if (name && patterns.some((pattern) => pattern.test(name)))
1488
+ found = true;
1489
+ else {
1490
+ const helper = bindings.get(node.callee) ?? functions.get(node.callee);
1491
+ if (helper)
1492
+ found = hasAssertion(helper, bindings, seen);
1493
+ }
1494
+ });
1495
+ return found;
1496
+ }
1497
+ for (const call of calls) {
1498
+ const name = testTablesCallName(call.callee);
1499
+ const argument = call.arguments[0];
1500
+ if (tests.some((test) => name === test + ".extend") && argument && argument.type !== "SpreadElement") {
1501
+ const fixtures = testTablesResolve(argument, values);
1502
+ if (fixtures.type === "ObjectExpression") {
1503
+ for (const property of fixtures.properties) {
1504
+ if (property.type !== "Property")
1505
+ continue;
1506
+ let value = property.value;
1507
+ if (value.type === "ArrayExpression" && value.elements[0])
1508
+ value = value.elements[0];
1509
+ const fn = functions.get(value) ?? value;
1510
+ if (fn.type === "FunctionExpression" || fn.type === "ArrowFunctionExpression" || fn.type === "FunctionDeclaration")
1511
+ allowed.add(fn);
1512
+ }
1513
+ }
1514
+ }
1515
+ const registration = testTablesRegistration(call, tests, functions);
1516
+ if (!registration)
1517
+ continue;
1518
+ allowed.add(registration.callback);
1519
+ const builder = call.callee.type === "CallExpression" ? testTablesCallName(call.callee.callee) : undefined;
1520
+ const parameter = registration.callback.params[builder?.endsWith(".for") ? 1 : 0];
1521
+ if (!builder?.endsWith(".each") && parameter?.type === "ObjectPattern") {
1522
+ const variables = context.sourceCode.getDeclaredVariables(registration.callback);
1523
+ for (const property of parameter.properties) {
1524
+ if (property.type !== "Property" || property.computed || property.key.type !== "Identifier" || property.key.name !== "expect" || property.value.type !== "Identifier")
1525
+ continue;
1526
+ const variable = variables.find((entry) => entry.identifiers.some((identifier) => identifier === property.value));
1527
+ for (const reference of variable?.references ?? []) {
1528
+ shadowed.delete(reference.identifier);
1529
+ aliases.set(reference.identifier, "expect");
1530
+ }
1531
+ }
1532
+ }
1533
+ const rows = testTablesRows(registration, context.sourceCode, values, functions);
1534
+ for (const row of rows ?? [])
1535
+ for (const callback of row.callbacks)
1536
+ allowed.add(callback);
1537
+ const valid = rows ? rows.length > 0 && rows.every((row) => hasAssertion(registration.callback, row.bindings, new Set)) : hasAssertion(registration.callback, new Map, new Set);
1538
+ if (!valid)
1539
+ context.report({ node: call, messageId: "missing" });
1540
+ }
1541
+ for (const call of calls) {
1542
+ const name = assertionName(call);
1543
+ if (!name || !testAssertionFunctions.includes(name) || !name.startsWith("expect"))
1544
+ continue;
1545
+ let parent = call.parent;
1546
+ let inside = false;
1547
+ while (parent && parent.type !== "Program") {
1548
+ if ((parent.type === "FunctionDeclaration" || parent.type === "FunctionExpression" || parent.type === "ArrowFunctionExpression") && allowed.has(parent)) {
1549
+ inside = true;
1550
+ break;
1551
+ }
1552
+ parent = parent.parent;
1553
+ }
1554
+ if (!inside)
1555
+ context.report({ node: call, messageId: "standalone" });
1556
+ }
1557
+ }
1558
+ };
1559
+ }
1560
+ });
1561
+
1562
+ // src/rules/module-policy/reasoned-directives.ts
1563
+ import { defineRule as defineRule7 } from "@oxlint/plugins";
1564
+ var directives = defineRule7({
1256
1565
  meta: {
1257
1566
  type: "problem",
1258
1567
  schema: [],
@@ -1281,8 +1590,8 @@ var directives = defineRule6({
1281
1590
 
1282
1591
  // src/rules/module-policy/tanstack-runtime.ts
1283
1592
  import { builtinModules } from "node:module";
1284
- import { defineRule as defineRule7 } from "@oxlint/plugins";
1285
- var tanstackRuntime = defineRule7({
1593
+ import { defineRule as defineRule8 } from "@oxlint/plugins";
1594
+ var tanstackRuntime = defineRule8({
1286
1595
  meta: {
1287
1596
  type: "problem",
1288
1597
  schema: [
@@ -1536,9 +1845,9 @@ var tanstackRuntime = defineRule7({
1536
1845
  });
1537
1846
 
1538
1847
  // src/rules/module-policy/import-boundaries.ts
1539
- import { defineRule as defineRule8 } from "@oxlint/plugins";
1848
+ import { defineRule as defineRule9 } from "@oxlint/plugins";
1540
1849
  import { resolve as resolve2, basename as basename2 } from "node:path";
1541
- var importBoundaries = defineRule8({
1850
+ var importBoundaries = defineRule9({
1542
1851
  meta: {
1543
1852
  type: "problem",
1544
1853
  schema: [
@@ -1674,8 +1983,8 @@ var importBoundaries = defineRule8({
1674
1983
  });
1675
1984
 
1676
1985
  // src/rules/module-policy/memoization.ts
1677
- import { defineRule as defineRule9 } from "@oxlint/plugins";
1678
- var memoization = defineRule9({
1986
+ import { defineRule as defineRule10 } from "@oxlint/plugins";
1987
+ var memoization = defineRule10({
1679
1988
  meta: {
1680
1989
  type: "problem",
1681
1990
  schema: [],
@@ -1718,6 +2027,7 @@ var memoization = defineRule9({
1718
2027
  var module_policy_default = eslintCompatPlugin({
1719
2028
  meta: { name: "modules" },
1720
2029
  rules: {
2030
+ "test-assertions": testAssertions,
1721
2031
  "dynamic-classes": syntaxRule(':matches(JSXAttribute[name.name="className"] TemplateLiteral[expressions.length>0], JSXAttribute[name.name="className"] BinaryExpression[operator="+"])', "Write complete class names instead of constructing them dynamically."),
1722
2032
  "backend-switch": syntaxRule("SwitchStatement", "Use ts-pattern in backend application code."),
1723
2033
  "concurrent-db": syntaxRule('CallExpression[callee.object.name="test"][callee.property.name="concurrent"] ObjectPattern > Property[key.name="db"]', "Obtain an isolated database fixture for concurrent tests."),
@@ -28,6 +28,7 @@ export interface PresetOptions {
28
28
  sql?: PresetSqlOptions;
29
29
  forms?: PresetFormsOptions;
30
30
  react?: PresetReactOptions;
31
+ tests?: PresetTestsOptions;
31
32
  policies?: PresetPolicyOptions;
32
33
  ignorePatterns?: string[];
33
34
  ruleExclusions?: Record<string, string[]>;
@@ -44,6 +45,12 @@ export interface PresetLimitsOptions {
44
45
  }
45
46
  export interface PresetModulesOptions {
46
47
  customFileRoles?: string[];
48
+ migrationFiles?: string[];
49
+ }
50
+ export interface PresetTestsOptions {
51
+ profile?: 'standard' | 'fixtures';
52
+ additionalTestFunctions?: string[];
53
+ additionalAssertionFunctions?: string[];
47
54
  }
48
55
  export interface PresetImportsOptions {
49
56
  backendEntryPoints?: string[];
@@ -63,6 +70,7 @@ export interface PresetFormsOptions {
63
70
  }
64
71
  export interface PresetReactOptions {
65
72
  compiler?: boolean;
73
+ componentProps?: boolean;
66
74
  }
67
75
  export interface PresetTanstackStartOptions {
68
76
  additionalServerOnlyImports?: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-arch",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Configurable architecture rules for Oxlint JavaScript plugins.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",