@jameswyse/oxc-config 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +30 -186
  2. package/REFERENCE.md +105 -132
  3. package/dist/index.js +26 -0
  4. package/dist/lint.d.ts +1 -0
  5. package/dist/lint.js +124 -4
  6. package/dist/rules/explicit-api-states.js +7 -1
  7. package/dist/rules/import-boundaries.js +31 -6
  8. package/dist/rules/no-derived-test-expectation.d.ts +1 -0
  9. package/dist/rules/no-derived-test-expectation.js +136 -0
  10. package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
  11. package/dist/rules/no-disabled-tls-verification.js +42 -0
  12. package/dist/rules/no-fixed-test-delay.js +46 -5
  13. package/dist/rules/no-generic-object-guard.d.ts +1 -0
  14. package/dist/rules/no-generic-object-guard.js +279 -0
  15. package/dist/rules/no-get-password-form.d.ts +1 -0
  16. package/dist/rules/no-get-password-form.js +113 -0
  17. package/dist/rules/no-hardcoded-credential.d.ts +1 -0
  18. package/dist/rules/no-hardcoded-credential.js +45 -0
  19. package/dist/rules/no-import-time-effects.d.ts +1 -0
  20. package/dist/rules/no-import-time-effects.js +90 -0
  21. package/dist/rules/no-optional-test-action.d.ts +1 -0
  22. package/dist/rules/no-optional-test-action.js +45 -0
  23. package/dist/rules/no-secret-fallback.d.ts +1 -0
  24. package/dist/rules/no-secret-fallback.js +64 -0
  25. package/dist/rules/no-swallowed-errors.js +131 -16
  26. package/dist/rules/no-test-timeout-overrides.js +22 -9
  27. package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
  28. package/dist/rules/no-unchecked-fetch-response.js +91 -0
  29. package/dist/rules/no-unchecked-index-search.d.ts +1 -0
  30. package/dist/rules/no-unchecked-index-search.js +41 -0
  31. package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
  32. package/dist/rules/no-uncontrolled-test-clock.js +112 -0
  33. package/dist/rules/no-unsettled-promise.d.ts +1 -0
  34. package/dist/rules/no-unsettled-promise.js +64 -0
  35. package/dist/rules/require-suppression-reason.d.ts +1 -0
  36. package/dist/rules/require-suppression-reason.js +32 -0
  37. package/dist/shared/index.d.ts +9 -0
  38. package/dist/shared/index.js +70 -0
  39. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
  40. package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
  41. package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
  42. package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
  43. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
  44. package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
  45. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
  46. package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
  47. package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
  48. package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
  49. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -10,15 +10,28 @@ import { noAmbientClock } from "./rules/no-ambient-clock.js";
10
10
  import { noAmbientRandom } from "./rules/no-ambient-random.js";
11
11
  import { noComments } from "./rules/no-comments.js";
12
12
  import { noDeepTernary } from "./rules/no-deep-ternary.js";
13
+ import { noDerivedTestExpectation } from "./rules/no-derived-test-expectation.js";
14
+ import { noDisabledTlsVerification } from "./rules/no-disabled-tls-verification.js";
13
15
  import { noEditProofAssertions } from "./rules/no-edit-proof-assertions.js";
14
16
  import { noEnvAccess } from "./rules/no-env-access.js";
15
17
  import { noFixedTestDelay } from "./rules/no-fixed-test-delay.js";
16
18
  import { noForwardingModule } from "./rules/no-forwarding-module.js";
17
19
  import { noGenericFileSuffix } from "./rules/no-generic-file-suffix.js";
20
+ import { noGenericObjectGuard } from "./rules/no-generic-object-guard.js";
21
+ import { noGetPasswordForm } from "./rules/no-get-password-form.js";
22
+ import { noHardcodedCredential } from "./rules/no-hardcoded-credential.js";
23
+ import { noImportTimeEffects } from "./rules/no-import-time-effects.js";
24
+ import { noOptionalTestAction } from "./rules/no-optional-test-action.js";
18
25
  import { noPassThroughFunction } from "./rules/no-pass-through-function.js";
26
+ import { noSecretFallback } from "./rules/no-secret-fallback.js";
19
27
  import { noSwallowedErrors } from "./rules/no-swallowed-errors.js";
20
28
  import { noTestTimeoutOverrides } from "./rules/no-test-timeout-overrides.js";
29
+ import { noUncheckedFetchResponse } from "./rules/no-unchecked-fetch-response.js";
30
+ import { noUncheckedIndexSearch } from "./rules/no-unchecked-index-search.js";
31
+ import { noUncontrolledTestClock } from "./rules/no-uncontrolled-test-clock.js";
32
+ import { noUnsettledPromise } from "./rules/no-unsettled-promise.js";
21
33
  import { noVacuousTestAssertions } from "./rules/no-vacuous-test-assertions.js";
34
+ import { requireSuppressionReason } from "./rules/require-suppression-reason.js";
22
35
  import { serverActionGuard } from "./rules/server-action-guard.js";
23
36
  import { statementSpacing } from "./rules/statement-spacing.js";
24
37
  import { typeGuardName } from "./rules/type-guard-name.js";
@@ -66,29 +79,42 @@ const wyse = definePlugin({
66
79
  "no-comments": noComments,
67
80
  "no-conditional-empty-object-spread": noConditionalEmptyObjectSpread,
68
81
  "no-deep-ternary": noDeepTernary,
82
+ "no-derived-test-expectation": noDerivedTestExpectation,
83
+ "no-disabled-tls-verification": noDisabledTlsVerification,
69
84
  "no-edit-proof-assertions": noEditProofAssertions,
70
85
  "no-env-access": noEnvAccess,
71
86
  "no-fixed-test-delay": noFixedTestDelay,
72
87
  "no-forwarding-module": noForwardingModule,
73
88
  "no-generic-file-suffix": noGenericFileSuffix,
89
+ "no-generic-object-guard": noGenericObjectGuard,
90
+ "no-get-password-form": noGetPasswordForm,
91
+ "no-hardcoded-credential": noHardcodedCredential,
92
+ "no-import-time-effects": noImportTimeEffects,
74
93
  "no-known-value-widening": noKnownValueWidening,
75
94
  "no-manual-effect-error-tag": noManualEffectErrorTag,
76
95
  "no-module-mocking": noModuleMocking,
77
96
  "no-object-parameters": noObjectParameters,
97
+ "no-optional-test-action": noOptionalTestAction,
78
98
  "no-pass-through-function": noPassThroughFunction,
79
99
  "no-reduce-accumulator-copy": noReduceAccumulatorCopy,
80
100
  "no-reflect-apply": noReflectApply,
81
101
  "no-reflect-get": noReflectGet,
82
102
  "no-runtime-typeof": noRuntimeTypeof,
103
+ "no-secret-fallback": noSecretFallback,
83
104
  "no-service-constructor-imports": noServiceConstructorImports,
84
105
  "no-shape-in-symbol-names": noForbiddenTermInSymbolNames,
85
106
  "no-swallowed-errors": noSwallowedErrors,
86
107
  "no-test-timeout-overrides": noTestTimeoutOverrides,
108
+ "no-unchecked-fetch-response": noUncheckedFetchResponse,
109
+ "no-unchecked-index-search": noUncheckedIndexSearch,
110
+ "no-uncontrolled-test-clock": noUncontrolledTestClock,
87
111
  "no-unknown-returns": noUnknownReturns,
88
112
  "no-unknown-type-aliases": noUnknownTypeAliases,
89
113
  "no-unsafe-dictionary-type": noUnsafeDictionaryType,
114
+ "no-unsettled-promise": noUnsettledPromise,
90
115
  "no-vacuous-test-assertions": noVacuousTestAssertions,
91
116
  "no-widen-then-assert": noWidenThenAssert,
117
+ "require-suppression-reason": requireSuppressionReason,
92
118
  "server-action-guard": serverActionGuard,
93
119
  "statement-spacing": statementSpacing,
94
120
  "type-guard-name": typeGuardName,
package/dist/lint.d.ts CHANGED
@@ -24,6 +24,7 @@ export interface ImportBoundary {
24
24
  ignore?: string[];
25
25
  deny: string[];
26
26
  message: string;
27
+ types?: boolean;
27
28
  }
28
29
  export type SharedLintConfig = OxlintConfig & {
29
30
  ignorePatterns: string[];
package/dist/lint.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  const require = createRequire(import.meta.url);
5
5
  const sourceFiles = ["**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"];
6
6
  const typeScriptFiles = ["**/*.{ts,tsx,mts,cts}"];
7
+ const javaScriptFiles = ["**/*.{js,jsx,mjs,cjs}"];
7
8
  const testFiles = [
8
9
  "**/*.{test,spec}.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
9
10
  "**/__tests__/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
@@ -35,12 +36,19 @@ const defaultExportFiles = [
35
36
  "**/*.stories.{js,jsx,mjs,ts,tsx,mts}",
36
37
  "**/.storybook/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
37
38
  ];
39
+ const programFiles = [
40
+ "**/{bin,cli,deploy,tools,test,tests,e2e,fixtures}/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
41
+ "**/.storybook/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
42
+ "**/{main,server,worker,instrumentation,instrumentation-client,healthcheck}.{js,jsx,mjs,cjs,ts,tsx,mts,cts}",
43
+ ];
38
44
  const endToEndFiles = ["**/e2e/**", "**/*.e2e.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"];
39
45
  const electronRendererFiles = ["src/renderer/**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}"];
40
46
  const testRules = {
41
47
  "wyse/no-fixed-test-delay": "error",
42
48
  "wyse/no-vacuous-test-assertions": "error",
43
49
  "wyse/no-edit-proof-assertions": "error",
50
+ "wyse/no-optional-test-action": "error",
51
+ "wyse/no-derived-test-expectation": "warn",
44
52
  "wyse/companion-has-owner": "warn",
45
53
  };
46
54
  const basePlugins = ["eslint", "unicorn", "oxc", "import", "promise", "typescript"];
@@ -70,6 +78,43 @@ function reactScope(react, nextjs) {
70
78
  const files = [...(react || []), ...(nextjs || [])];
71
79
  return files.length > 0 ? [...new Set(files)] : false;
72
80
  }
81
+ const nativeGuards = {
82
+ ...errors("eslint", [
83
+ "array-callback-return",
84
+ "no-array-constructor",
85
+ "no-case-declarations",
86
+ "no-constructor-return",
87
+ "no-fallthrough",
88
+ "no-loop-func",
89
+ "no-new-func",
90
+ "no-new-wrappers",
91
+ "no-proto",
92
+ "no-prototype-builtins",
93
+ "no-self-compare",
94
+ "no-unreachable-loop",
95
+ "no-useless-assignment",
96
+ "no-var",
97
+ "prefer-object-has-own",
98
+ "prefer-rest-params",
99
+ ]),
100
+ "eslint/prefer-regex-literals": ["error", { disallowRedundantWrapping: true }],
101
+ ...errors("unicorn", [
102
+ "error-message",
103
+ "new-for-builtins",
104
+ "no-instanceof-array",
105
+ "no-new-buffer",
106
+ "prefer-array-find",
107
+ "prefer-array-flat-map",
108
+ "prefer-array-some",
109
+ "prefer-import-meta-properties",
110
+ "prefer-node-protocol",
111
+ "prefer-response-static-json",
112
+ "prefer-string-trim-start-end",
113
+ ]),
114
+ "import/no-mutable-exports": "error",
115
+ "oxc/no-const-enum": "error",
116
+ };
117
+ const javaScriptOnlyRules = errors("eslint", ["no-throw-literal", "prefer-promise-reject-errors"]);
73
118
  const baseErrors = errors("wyse", [
74
119
  "no-array-filter-map",
75
120
  "no-reduce-accumulator-copy",
@@ -99,10 +144,23 @@ const typeAwareRules = {
99
144
  "no-unsafe-return",
100
145
  "no-unsafe-type-assertion",
101
146
  "only-throw-error",
147
+ "prefer-promise-reject-errors",
102
148
  "prefer-readonly",
149
+ "prefer-reduce-type-parameter",
103
150
  "return-await",
104
151
  "switch-exhaustiveness-check",
152
+ "use-unknown-in-catch-callback-variable",
105
153
  ]),
154
+ "typescript/restrict-plus-operands": [
155
+ "error",
156
+ {
157
+ allowAny: false,
158
+ allowBoolean: false,
159
+ allowNullish: false,
160
+ allowNumberAndString: false,
161
+ allowRegExp: false,
162
+ },
163
+ ],
106
164
  "typescript/no-floating-promises": [
107
165
  "error",
108
166
  {
@@ -250,7 +308,14 @@ const testRuleNames = [
250
308
  "no-duplicate-hooks",
251
309
  "no-focused-tests",
252
310
  "no-identical-title",
311
+ "no-interpolation-in-snapshots",
253
312
  "no-standalone-expect",
313
+ "no-unneeded-async-expect-function",
314
+ "prefer-comparison-matcher",
315
+ "prefer-equality-matcher",
316
+ "prefer-strict-equal",
317
+ "prefer-to-contain",
318
+ "prefer-to-have-length",
254
319
  "valid-describe-callback",
255
320
  "valid-expect",
256
321
  "valid-expect-in-promise",
@@ -273,13 +338,32 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
273
338
  const jsPlugins = [wysePlugin];
274
339
  const typescriptRules = {
275
340
  ...errors("typescript", [
276
- "ban-ts-comment",
277
341
  "consistent-type-imports",
278
342
  "no-explicit-any",
279
343
  "no-non-null-assertion",
280
344
  "no-empty-object-type",
281
345
  "no-unnecessary-type-constraint",
282
346
  ]),
347
+ "typescript/ban-ts-comment": [
348
+ "error",
349
+ {
350
+ "ts-expect-error": { descriptionFormat: "^\\s*-{2,}\\s+\\S" },
351
+ "ts-ignore": true,
352
+ "ts-nocheck": true,
353
+ "ts-check": false,
354
+ },
355
+ ],
356
+ "typescript/consistent-type-assertions": [
357
+ "error",
358
+ {
359
+ assertionStyle: "as",
360
+ objectLiteralTypeAssertions: "never",
361
+ arrayLiteralTypeAssertions: "never",
362
+ },
363
+ ],
364
+ "typescript/method-signature-style": ["error", "property"],
365
+ "typescript/no-import-type-side-effects": "error",
366
+ "typescript/no-unsafe-function-type": "error",
283
367
  "wyse/type-guard-name": "error",
284
368
  };
285
369
  const overrides = [
@@ -297,11 +381,28 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
297
381
  files: defaultExportFiles,
298
382
  rules: { "import/no-default-export": "off" },
299
383
  },
384
+ { files: javaScriptFiles, rules: javaScriptOnlyRules },
300
385
  { files: allTestFiles, rules: testRules },
301
386
  {
302
387
  files: allTestFiles,
303
388
  excludeFiles: endToEndFiles,
304
- rules: { "wyse/no-test-timeout-overrides": "warn" },
389
+ rules: {
390
+ "wyse/no-test-timeout-overrides": "warn",
391
+ "wyse/no-uncontrolled-test-clock": "error",
392
+ },
393
+ },
394
+ {
395
+ files: sourceFiles,
396
+ excludeFiles: allTestFiles,
397
+ rules: {
398
+ "wyse/no-unchecked-fetch-response": "error",
399
+ "wyse/no-unsettled-promise": "error",
400
+ },
401
+ },
402
+ {
403
+ files: sourceFiles,
404
+ excludeFiles: [...allTestFiles, ...entrypointFiles, ...programFiles],
405
+ rules: { "wyse/no-import-time-effects": "warn" },
305
406
  },
306
407
  ];
307
408
  if (env !== undefined && env.length > 0) {
@@ -351,7 +452,11 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
351
452
  });
352
453
  }
353
454
  if (typeAware) {
354
- overrides.unshift({ files: typeScriptFiles, rules: typeAwareRules });
455
+ overrides.unshift({ files: typeScriptFiles, rules: typeAwareRules }, {
456
+ files: typeScriptFiles,
457
+ excludeFiles: allTestFiles,
458
+ rules: { "typescript/no-deprecated": "warn" },
459
+ });
355
460
  }
356
461
  if (node) {
357
462
  overrides.push({
@@ -379,6 +484,7 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
379
484
  ...accessibilityRules,
380
485
  "react/exhaustive-deps": "error",
381
486
  "react/no-array-index-key": "error",
487
+ "wyse/no-get-password-form": "error",
382
488
  "react/react-in-jsx-scope": "off",
383
489
  "jsx-a11y/control-has-associated-label": [
384
490
  "error",
@@ -398,11 +504,18 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
398
504
  "jsx-a11y/label-has-associated-control": ["error", { depth: 25 }],
399
505
  "jsx-a11y/no-noninteractive-tabindex": ["error", { roles: ["region", "tabpanel"] }],
400
506
  "jsx-a11y/prefer-tag-over-role": "off",
507
+ "react/checked-requires-onchange-or-readonly": "error",
508
+ "react/jsx-no-useless-fragment": ["error", { allowExpressions: true }],
509
+ "react/no-unknown-property": "error",
401
510
  },
402
511
  }, {
403
512
  files: reactFiles,
404
513
  excludeFiles: allTestFiles,
405
514
  rules: { ...reactEffectRules, ...browserPerformanceRules },
515
+ }, {
516
+ files: reactFiles,
517
+ excludeFiles: [...endToEndFiles, "**/tests/**"],
518
+ rules: { "react/rules-of-hooks": "error" },
406
519
  });
407
520
  }
408
521
  if (nextFiles.length > 0) {
@@ -433,7 +546,7 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
433
546
  plugins: [...basePlugins, name],
434
547
  env: { [name]: true },
435
548
  rules: {
436
- ...errors(name, testRuleNames),
549
+ ...errors(name, name === "jest" ? [...testRuleNames, "no-done-callback"] : testRuleNames),
437
550
  ...testRules,
438
551
  [`${name}/no-standalone-expect`]: [
439
552
  "error",
@@ -457,12 +570,19 @@ export function createLintConfig({ root, typeAware = true, node = false, react =
457
570
  env: { es2022: true },
458
571
  ignorePatterns: defaultIgnores,
459
572
  rules: {
573
+ ...nativeGuards,
460
574
  ...baseErrors,
461
575
  ...typescriptRules,
462
576
  "wyse/no-runtime-typeof": ["warn", { allowInTypeGuards: true }],
463
577
  "wyse/no-comments": "error",
578
+ "wyse/require-suppression-reason": "error",
464
579
  "wyse/statement-spacing": "error",
465
580
  "wyse/no-generic-file-suffix": "error",
581
+ "wyse/no-generic-object-guard": "error",
582
+ "wyse/no-disabled-tls-verification": "error",
583
+ "wyse/no-hardcoded-credential": "error",
584
+ "wyse/no-secret-fallback": "error",
585
+ "wyse/no-unchecked-index-search": "error",
466
586
  "wyse/no-swallowed-errors": "warn",
467
587
  "wyse/explicit-api-states": "warn",
468
588
  "wyse/no-pass-through-function": "warn",
@@ -29,6 +29,12 @@ function isSetter(name, fn, parameter) {
29
29
  return (/^(?:set|update)\p{Lu}/u.test(name.slice(name.lastIndexOf(".") + 1)) &&
30
30
  fn.params.at(-1) === parameter);
31
31
  }
32
+ function isPredicateSubject(fn, identifier) {
33
+ const predicate = fn.returnType?.typeAnnotation;
34
+ return (predicate?.type === "TSTypePredicate" &&
35
+ predicate.parameterName.type === "Identifier" &&
36
+ predicate.parameterName.name === identifier.name);
37
+ }
32
38
  function functionValue(node) {
33
39
  return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression"
34
40
  ? node
@@ -92,7 +98,7 @@ export const explicitApiStates = defineRule({
92
98
  }
93
99
  for (const parameter of fn.params) {
94
100
  const identifier = booleanParameter(parameter);
95
- if (identifier && !isSetter(name, fn, parameter)) {
101
+ if (identifier && !isSetter(name, fn, parameter) && !isPredicateSubject(fn, identifier)) {
96
102
  context.report({
97
103
  node: identifier,
98
104
  messageId: "boolean",
@@ -1,6 +1,6 @@
1
1
  import { dirname, resolve as resolvePath } from "node:path";
2
2
  import { defineRule } from "@oxlint/plugins";
3
- import { projectPath, matchesAny } from "../shared/index.js";
3
+ import { importedBinding, isGlobal, matchesAny, projectPath, variableFor, } from "../shared/index.js";
4
4
  function isStringList(value) {
5
5
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6
6
  }
@@ -8,14 +8,15 @@ function parseBoundary(value) {
8
8
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
9
9
  throw new TypeError("Each import boundary must be an object.");
10
10
  }
11
- const { files, ignore = [], deny, message } = value;
11
+ const { files, ignore = [], deny, message, types = false } = value;
12
12
  if (!isStringList(files) ||
13
13
  !isStringList(ignore) ||
14
14
  !isStringList(deny) ||
15
- typeof message !== "string") {
15
+ typeof message !== "string" ||
16
+ typeof types !== "boolean") {
16
17
  throw new TypeError("Each import boundary needs files, deny and message settings.");
17
18
  }
18
- return { files, ignore, deny, message };
19
+ return { files, ignore, deny, message, types };
19
20
  }
20
21
  function parseSettings(options) {
21
22
  const [option] = options;
@@ -28,6 +29,17 @@ function parseSettings(options) {
28
29
  }
29
30
  return { root: option.root, boundaries: option.boundaries.map(parseBoundary) };
30
31
  }
32
+ const nodeModuleSources = new Set(["module", "node:module"]);
33
+ function isRequire(sourceCode, callee) {
34
+ if (isGlobal(sourceCode, callee, "require")) {
35
+ return true;
36
+ }
37
+ const definition = variableFor(sourceCode, callee)?.defs[0];
38
+ const init = definition?.node.type === "VariableDeclarator" ? definition.node.init : null;
39
+ const factory = init?.type === "CallExpression" ? init.callee : null;
40
+ const binding = factory === null ? null : importedBinding(sourceCode, factory);
41
+ return (binding !== null && nodeModuleSources.has(binding.source) && binding.name === "createRequire");
42
+ }
31
43
  export const importBoundaries = defineRule({
32
44
  meta: {
33
45
  type: "problem",
@@ -46,6 +58,7 @@ export const importBoundaries = defineRule({
46
58
  ignore: { type: "array", items: { type: "string" } },
47
59
  deny: { type: "array", items: { type: "string" } },
48
60
  message: { type: "string" },
61
+ types: { type: "boolean" },
49
62
  },
50
63
  required: ["files", "deny", "message"],
51
64
  additionalProperties: false,
@@ -66,14 +79,14 @@ export const importBoundaries = defineRule({
66
79
  return {};
67
80
  }
68
81
  function check(source, typeOnly) {
69
- if (typeOnly || source?.type !== "Literal" || typeof source.value !== "string") {
82
+ if (source?.type !== "Literal" || typeof source.value !== "string") {
70
83
  return;
71
84
  }
72
85
  const specifier = source.value;
73
86
  const target = specifier.startsWith(".")
74
87
  ? projectPath(root, resolvePath(dirname(context.filename), specifier))
75
88
  : specifier;
76
- const boundary = active.find(({ deny }) => matchesAny(target, deny));
89
+ const boundary = active.find(({ deny, types }) => (types || !typeOnly) && matchesAny(target, deny));
77
90
  if (boundary) {
78
91
  context.report({
79
92
  node: source,
@@ -97,6 +110,18 @@ export const importBoundaries = defineRule({
97
110
  ImportExpression(node) {
98
111
  check(node.source, false);
99
112
  },
113
+ TSImportType(node) {
114
+ check(node.source, true);
115
+ },
116
+ TSExternalModuleReference(node) {
117
+ check(node.expression, node.parent.type === "TSImportEqualsDeclaration" && node.parent.importKind === "type");
118
+ },
119
+ CallExpression(node) {
120
+ const [source] = node.arguments;
121
+ if (node.arguments.length === 1 && isRequire(context.sourceCode, node.callee)) {
122
+ check(source, false);
123
+ }
124
+ },
100
125
  };
101
126
  },
102
127
  });
@@ -0,0 +1 @@
1
+ export declare const noDerivedTestExpectation: import("@oxlint/plugins").Rule;
@@ -0,0 +1,136 @@
1
+ import { dirname, join } from "node:path";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { apiReference, childNodes, projectPath, propertyName, variableFor, } from "../shared/index.js";
4
+ const comparisonMatchers = new Set([
5
+ "toBe",
6
+ "toEqual",
7
+ "toStrictEqual",
8
+ "toMatchObject",
9
+ "toContain",
10
+ "toContainEqual",
11
+ "toMatch",
12
+ "toHaveProperty",
13
+ "toBeCloseTo",
14
+ ]);
15
+ const testSupport = /(?:^|[/.])(?:tests?|__tests__|e2e|testing|fixtures?|mocks?|fakes?|stubs?|builders?|factories|support|helpers?)(?:[/.]|$)/u;
16
+ const projectSpecifier = /^(?:\.{1,2}\/|@\/|~\/|#)/u;
17
+ function isProductionImport(sourceCode, directory, callee) {
18
+ const definition = variableFor(sourceCode, callee)?.defs[0];
19
+ const declaration = definition?.parent;
20
+ if (definition?.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
21
+ return false;
22
+ }
23
+ const source = declaration.source.value;
24
+ const path = source.startsWith(".") ? join(directory, source) : source;
25
+ return projectSpecifier.test(source) && !testSupport.test(path.replaceAll("\\", "/"));
26
+ }
27
+ function calledName(sourceCode, directory, callee) {
28
+ if (callee.type === "Identifier") {
29
+ return isProductionImport(sourceCode, directory, callee) ? callee.name : null;
30
+ }
31
+ const name = callee.type === "MemberExpression" ? propertyName(callee) : null;
32
+ const namespace = callee.type === "MemberExpression" ? callee.object : null;
33
+ return namespace?.type === "Identifier" &&
34
+ name !== null &&
35
+ variableFor(sourceCode, namespace)?.defs[0]?.node.type === "ImportNamespaceSpecifier" &&
36
+ isProductionImport(sourceCode, directory, namespace)
37
+ ? `${namespace.name}.${name}`
38
+ : null;
39
+ }
40
+ function constantValue(sourceCode, node) {
41
+ const variable = node.type === "Identifier" ? variableFor(sourceCode, node) : null;
42
+ const definition = variable?.defs[0];
43
+ return variable !== null &&
44
+ definition?.type === "Variable" &&
45
+ definition.parent?.type === "VariableDeclaration" &&
46
+ definition.parent.kind === "const" &&
47
+ definition.node.type === "VariableDeclarator" &&
48
+ definition.node.id.type === "Identifier" &&
49
+ definition.node.init !== null
50
+ ? { variable, value: definition.node.init }
51
+ : null;
52
+ }
53
+ function isExpectedArgument(node) {
54
+ const call = node.parent;
55
+ return (call?.type === "CallExpression" &&
56
+ call.arguments.some((argument) => argument === node) &&
57
+ call.callee.type === "MemberExpression" &&
58
+ comparisonMatchers.has(propertyName(call.callee) ?? ""));
59
+ }
60
+ function subjectRoots(sourceCode, argument) {
61
+ const constant = constantValue(sourceCode, argument);
62
+ return constant === null ? [argument] : [argument, constant.value];
63
+ }
64
+ function expectedRoot(sourceCode, argument) {
65
+ const constant = constantValue(sourceCode, argument);
66
+ return constant !== null &&
67
+ constant.variable.references.every((reference) => !reference.isRead() || isExpectedArgument(reference.identifier))
68
+ ? constant.value
69
+ : argument;
70
+ }
71
+ function productionCalls(sourceCode, directory, root) {
72
+ const names = [];
73
+ const pending = [root];
74
+ for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
75
+ const name = node.type === "CallExpression" ? calledName(sourceCode, directory, node.callee) : null;
76
+ if (name !== null) {
77
+ names.push(name);
78
+ }
79
+ if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") {
80
+ pending.push(...childNodes(node, sourceCode.visitorKeys));
81
+ }
82
+ }
83
+ return names;
84
+ }
85
+ function positiveExpectation(sourceCode, node) {
86
+ let current = node;
87
+ while (current.type === "MemberExpression") {
88
+ if (propertyName(current) === "not") {
89
+ return null;
90
+ }
91
+ current = current.object;
92
+ }
93
+ return current.type === "CallExpression" &&
94
+ apiReference(sourceCode, current.callee)?.name === "expect"
95
+ ? current
96
+ : null;
97
+ }
98
+ export const noDerivedTestExpectation = defineRule({
99
+ meta: {
100
+ type: "suggestion",
101
+ docs: {
102
+ description: "Disallow expected values computed by the production code under test.",
103
+ },
104
+ schema: [],
105
+ messages: {
106
+ derived: "The expected value calls `{{name}}` from production code, so a bug in it changes both sides and the assertion still passes. Write the expected value out, or derive it independently.",
107
+ },
108
+ },
109
+ create(context) {
110
+ const sourceCode = context.sourceCode;
111
+ const directory = dirname(projectPath(context.cwd, context.filename));
112
+ return {
113
+ CallExpression(node) {
114
+ const matcher = node.callee.type === "MemberExpression" ? propertyName(node.callee) : null;
115
+ const expectation = node.callee.type === "MemberExpression" &&
116
+ matcher !== null &&
117
+ comparisonMatchers.has(matcher)
118
+ ? positiveExpectation(sourceCode, node.callee.object)
119
+ : null;
120
+ if (expectation === null) {
121
+ return;
122
+ }
123
+ const subjectCalls = new Set(expectation.arguments
124
+ .flatMap((argument) => subjectRoots(sourceCode, argument))
125
+ .flatMap((root) => productionCalls(sourceCode, directory, root)));
126
+ for (const argument of node.arguments) {
127
+ const name = productionCalls(sourceCode, directory, expectedRoot(sourceCode, argument)).find((call) => !subjectCalls.has(call));
128
+ if (name !== undefined) {
129
+ context.report({ node: argument, messageId: "derived", data: { name } });
130
+ return;
131
+ }
132
+ }
133
+ },
134
+ };
135
+ },
136
+ });
@@ -0,0 +1 @@
1
+ export declare const noDisabledTlsVerification: import("@oxlint/plugins").Rule;
@@ -0,0 +1,42 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { isGlobal, staticPropertyName, unwrapExpression } from "../shared/index.js";
3
+ function isFalse(node) {
4
+ const value = unwrapExpression(node);
5
+ return value.type === "Literal" && value.value === false;
6
+ }
7
+ function isDisabledFlag(node) {
8
+ const value = unwrapExpression(node);
9
+ return (value.type === "Literal" && (value.value === "0" || value.value === 0)) || isFalse(value);
10
+ }
11
+ export const noDisabledTlsVerification = defineRule({
12
+ meta: {
13
+ type: "problem",
14
+ docs: { description: "Disallow turning off TLS certificate verification." },
15
+ schema: [],
16
+ messages: {
17
+ disabled: "This turns off TLS certificate verification, so any server can impersonate the real one. Trust the right certificate authority instead, such as with the `ca` option or `NODE_EXTRA_CA_CERTS`.",
18
+ },
19
+ },
20
+ create(context) {
21
+ const sourceCode = context.sourceCode;
22
+ return {
23
+ Property(node) {
24
+ if (staticPropertyName(node) === "rejectUnauthorized" && isFalse(node.value)) {
25
+ context.report({ node, messageId: "disabled" });
26
+ }
27
+ },
28
+ AssignmentExpression(node) {
29
+ const target = unwrapExpression(node.left);
30
+ const environment = target.type === "MemberExpression" ? unwrapExpression(target.object) : null;
31
+ if (target.type === "MemberExpression" &&
32
+ staticPropertyName(target) === "NODE_TLS_REJECT_UNAUTHORIZED" &&
33
+ environment?.type === "MemberExpression" &&
34
+ staticPropertyName(environment) === "env" &&
35
+ isGlobal(sourceCode, unwrapExpression(environment.object), "process") &&
36
+ isDisabledFlag(node.right)) {
37
+ context.report({ node, messageId: "disabled" });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ });