oxlint-plugin-effect 0.12.1 → 0.14.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 (47) hide show
  1. package/README.md +25 -14
  2. package/THIRD_PARTY_NOTICES.md +4 -2
  3. package/dist/presets/recommended.d.ts +9 -0
  4. package/dist/presets/recommended.js +10 -1
  5. package/dist/rules/_anti-slop-array-method.js +70 -0
  6. package/dist/rules/_anti-slop-dictionary-types.js +58 -53
  7. package/dist/rules/_anti-slop-function-parameters.js +27 -0
  8. package/dist/rules/_anti-slop-lexical-type-parameters.js +33 -0
  9. package/dist/rules/_anti-slop-reflect-method.js +21 -0
  10. package/dist/rules/_anti-slop-scope.js +13 -0
  11. package/dist/rules/_anti-slop-type-alias-resolution.js +146 -0
  12. package/dist/rules/_function-metrics.js +31 -31
  13. package/dist/rules/_test-files.js +8 -0
  14. package/dist/rules/index.d.ts +10 -1
  15. package/dist/rules/index.js +19 -1
  16. package/dist/rules/no-array-filter-map.d.ts +7 -0
  17. package/dist/rules/no-array-filter-map.js +39 -0
  18. package/dist/rules/no-as.js +12 -6
  19. package/dist/rules/no-conditional-empty-object-spread.d.ts +1 -1
  20. package/dist/rules/no-conditional-empty-object-spread.js +8 -51
  21. package/dist/rules/no-eager-acquire.d.ts +6 -0
  22. package/dist/rules/no-eager-acquire.js +92 -0
  23. package/dist/rules/no-effect-run-in-tests.d.ts +6 -0
  24. package/dist/rules/no-effect-run-in-tests.js +57 -0
  25. package/dist/rules/no-known-value-widening.js +6 -14
  26. package/dist/rules/no-module-level-mutable-state.d.ts +6 -0
  27. package/dist/rules/no-module-level-mutable-state.js +51 -0
  28. package/dist/rules/no-object-parameters.js +11 -27
  29. package/dist/rules/no-reduce-accumulator-copy.d.ts +7 -0
  30. package/dist/rules/no-reduce-accumulator-copy.js +94 -0
  31. package/dist/rules/no-reflect-apply.d.ts +7 -0
  32. package/dist/rules/no-reflect-apply.js +25 -0
  33. package/dist/rules/no-reflect-get.d.ts +7 -0
  34. package/dist/rules/no-reflect-get.js +25 -0
  35. package/dist/rules/no-runtime-typeof.js +14 -1
  36. package/dist/rules/no-shape-in-symbol-names.js +7 -1
  37. package/dist/rules/no-unknown-parameters.d.ts +1 -1
  38. package/dist/rules/no-unknown-parameters.js +23 -16
  39. package/dist/rules/no-unknown-returns.d.ts +7 -0
  40. package/dist/rules/no-unknown-returns.js +73 -0
  41. package/dist/rules/no-unknown-type-aliases.js +17 -27
  42. package/dist/rules/no-unsafe-dictionary-type.js +54 -3
  43. package/dist/rules/no-widen-then-assert.js +1 -1
  44. package/dist/rules/require-suppression-reason.d.ts +6 -0
  45. package/dist/rules/require-suppression-reason.js +87 -0
  46. package/dist/vendor/effect-oxlint/Diagnostic.js +1 -1
  47. package/package.json +5 -4
@@ -2,13 +2,9 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { createTypeAliasEnvironment, resolvedTypeMatches } from "./_anti-slop-type-alias-resolution.js";
5
6
  import * as Effect from "effect/Effect";
6
7
  //#region src/rules/no-unknown-type-aliases.ts
7
- function referencedAliasName(type) {
8
- if (type.type === "TSParenthesizedType") return referencedAliasName(type.typeAnnotation);
9
- if (type.type !== "TSTypeReference" || type.typeName.type !== "Identifier") return null;
10
- return type.typeArguments === null || type.typeArguments === void 0 || type.typeArguments.params.length === 0 ? type.typeName.name : null;
11
- }
12
8
  /** Ban named aliases that merely conceal TypeScript's unknown top type. */
13
9
  const noUnknownTypeAliases = define({
14
10
  name: "no-unknown-type-aliases",
@@ -19,29 +15,23 @@ const noUnknownTypeAliases = define({
19
15
  }),
20
16
  create: function* () {
21
17
  const context = yield* RuleContext;
22
- const aliases = /* @__PURE__ */ new Map();
23
- const resolvesToUnknown = (type, visited = /* @__PURE__ */ new Set()) => {
24
- if (type.type === "TSUnknownKeyword") return true;
25
- if (type.type === "TSParenthesizedType") return resolvesToUnknown(type.typeAnnotation, visited);
26
- const name = referencedAliasName(type);
27
- if (name === null || visited.has(name)) return false;
28
- const alias = aliases.get(name);
29
- if (alias === void 0 || alias.typeParameters !== null && alias.typeParameters !== void 0) return false;
30
- const nextVisited = new Set(visited);
31
- nextVisited.add(name);
32
- return resolvesToUnknown(alias.typeAnnotation, nextVisited);
33
- };
34
- return { Program: (node) => {
35
- for (const statement of node.body) {
36
- const declaration = statement.type === "ExportNamedDeclaration" ? statement.declaration : statement;
37
- if (declaration?.type === "TSTypeAliasDeclaration") aliases.set(declaration.id.name, declaration);
38
- }
39
- return Effect.forEach(aliases.values(), (alias) => resolvesToUnknown(alias.typeAnnotation, new Set([alias.id.name])) ? context.report(fromId({
40
- node: alias.id,
18
+ let environment = null;
19
+ const resolvesToUnknown = (type) => environment !== null && resolvedTypeMatches(type, environment, (resolved, matches) => {
20
+ if (resolved.type === "TSUnknownKeyword") return true;
21
+ if (resolved.type === "TSParenthesizedType") return matches(resolved.typeAnnotation);
22
+ return resolved.type === "TSUnionType" && resolved.types.some(matches);
23
+ });
24
+ return {
25
+ Program: (node) => {
26
+ environment = createTypeAliasEnvironment(node, context.sourceCode.visitorKeys);
27
+ return Effect.void;
28
+ },
29
+ TSTypeAliasDeclaration: (node) => resolvesToUnknown(node.typeAnnotation) ? context.report(fromId({
30
+ node: node.id,
41
31
  messageId: "unknownAlias",
42
- data: { alias: alias.id.name }
43
- })) : Effect.void, { discard: true });
44
- } };
32
+ data: { alias: node.id.name }
33
+ })) : Effect.void
34
+ };
45
35
  }
46
36
  });
47
37
  //#endregion
@@ -2,11 +2,51 @@ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
+ import { visibleTypeAlias } from "./_anti-slop-type-alias-resolution.js";
5
6
  import { classifyUnsafeDictionary, classifyUnsafeDictionaryValue, createTypeEnvironment } from "./_anti-slop-dictionary-types.js";
6
7
  import * as Effect from "effect/Effect";
7
8
  //#region src/rules/no-unsafe-dictionary-type.ts
9
+ const typeNodeKinds = new Set([
10
+ "JSDocNonNullableType",
11
+ "JSDocNullableType",
12
+ "JSDocUnknownType",
13
+ "TSAnyKeyword",
14
+ "TSArrayType",
15
+ "TSBigIntKeyword",
16
+ "TSBooleanKeyword",
17
+ "TSConditionalType",
18
+ "TSConstructorType",
19
+ "TSFunctionType",
20
+ "TSImportType",
21
+ "TSIndexedAccessType",
22
+ "TSInferType",
23
+ "TSIntersectionType",
24
+ "TSIntrinsicKeyword",
25
+ "TSLiteralType",
26
+ "TSMappedType",
27
+ "TSNamedTupleMember",
28
+ "TSNeverKeyword",
29
+ "TSNullKeyword",
30
+ "TSNumberKeyword",
31
+ "TSObjectKeyword",
32
+ "TSParenthesizedType",
33
+ "TSStringKeyword",
34
+ "TSSymbolKeyword",
35
+ "TSTemplateLiteralType",
36
+ "TSThisType",
37
+ "TSTupleType",
38
+ "TSTypeLiteral",
39
+ "TSTypeOperator",
40
+ "TSTypePredicate",
41
+ "TSTypeQuery",
42
+ "TSTypeReference",
43
+ "TSUndefinedKeyword",
44
+ "TSUnionType",
45
+ "TSUnknownKeyword",
46
+ "TSVoidKeyword"
47
+ ]);
8
48
  function isTypeNode(node) {
9
- return node.type.startsWith("TS") && node.type !== "TSTypeAnnotation";
49
+ return typeNodeKinds.has(node.type);
10
50
  }
11
51
  function typeReferenceName(type) {
12
52
  return type.typeName.type === "Identifier" ? type.typeName.name : null;
@@ -22,9 +62,20 @@ function isInsideTypeAliasDeclaration(node) {
22
62
  function isPlainAliasConsumerUse(node, environment) {
23
63
  if (node.type !== "TSTypeReference" || node.typeArguments?.params.length) return false;
24
64
  const name = typeReferenceName(node);
25
- return name !== null && environment.aliases.has(name) && !isInsideTypeAliasDeclaration(node);
65
+ return name !== null && visibleTypeAlias(name, node, environment.typeAliases) !== null && !isInsideTypeAliasDeclaration(node);
66
+ }
67
+ function isInsideTypeParameterConstraint(node) {
68
+ let child = node;
69
+ let parent = child.parent;
70
+ while (parent !== null && parent.type !== "Program") {
71
+ if (parent.type === "TSTypeParameter" && parent.constraint === child) return true;
72
+ child = parent;
73
+ parent = child.parent;
74
+ }
75
+ return false;
26
76
  }
27
77
  function shouldReportType(node, environment) {
78
+ if (isInsideTypeParameterConstraint(node)) return false;
28
79
  if (isPlainAliasConsumerUse(node, environment)) return false;
29
80
  if (classifyUnsafeDictionary(node, environment) === null) return false;
30
81
  let current = node.parent;
@@ -57,7 +108,7 @@ const noUnsafeDictionaryType = define({
57
108
  };
58
109
  return {
59
110
  Program: (node) => {
60
- environment = createTypeEnvironment(node);
111
+ environment = createTypeEnvironment(node, context.sourceCode.visitorKeys);
61
112
  return Effect.void;
62
113
  },
63
114
  TSTypeReference: reportIfUnsafe,
@@ -6,7 +6,7 @@ import * as Effect from "effect/Effect";
6
6
  //#region src/rules/no-widen-then-assert.ts
7
7
  /**
8
8
  * Ported from dmmulroy/anti-slop at
9
- * b5d2288db1f00469a1d5f2e3b0e265e5a5676fd0.
9
+ * c44ef22ca116d0ba62a3ff663a0bd13a3f3fa40b.
10
10
  */
11
11
  const functionBoundaryTypes = new Set([
12
12
  "ArrowFunctionExpression",
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/require-suppression-reason.d.ts
4
+ declare const requireSuppressionReason: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { requireSuppressionReason };
@@ -0,0 +1,87 @@
1
+ import { fromId } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import * as Effect from "effect/Effect";
6
+ //#region src/rules/require-suppression-reason.ts
7
+ /**
8
+ * A directive at the start of a comment line, after any JSDoc `*` gutter.
9
+ * The rules and reason run to the next directive or the end of the comment.
10
+ */
11
+ const directivePattern = /(?:^|\n)[\t *]*(?<name>(?:oxlint|eslint)-disable(?:-next-line|-line)?|@effect-diagnostics(?:-next-line)?|@ts-(?:expect-error|ignore|nocheck))(?=$|[\s:])/gu;
12
+ /** ESLint-style reason separator: `--` with whitespace on both sides. */
13
+ const reasonSeparator = /\s--\s/u;
14
+ const kindOf = (name) => {
15
+ if (name.startsWith("@ts-")) return "typescript";
16
+ if (name.startsWith("@effect-diagnostics")) return "effect-diagnostics";
17
+ return "lint";
18
+ };
19
+ const parseDirectives = (value) => {
20
+ const matches = [...value.matchAll(directivePattern)];
21
+ return matches.map((match, index) => {
22
+ const name = match.groups?.["name"] ?? "";
23
+ const start = match.index + match[0].length;
24
+ const end = matches[index + 1]?.index ?? value.length;
25
+ const padded = ` ${value.slice(start, end).replaceAll(/\n[\t *]*/gu, " ").trim()} `;
26
+ const separator = padded.search(reasonSeparator);
27
+ const targetText = separator === -1 ? padded : padded.slice(0, separator);
28
+ const reason = separator === -1 ? "" : padded.slice(separator + 4);
29
+ return {
30
+ kind: kindOf(name),
31
+ name,
32
+ targets: targetText.split(/[\s,]+/u).filter((target) => target.length > 0),
33
+ reason: reason.trim()
34
+ };
35
+ });
36
+ };
37
+ const checkDirective = (directive) => {
38
+ const findings = [];
39
+ if (directive.kind !== "typescript" && directive.targets.length === 0) {
40
+ const example = directive.kind === "lint" ? "rule-name" : "ruleName:off";
41
+ findings.push({
42
+ messageId: "missingTarget",
43
+ data: {
44
+ directive: directive.name,
45
+ example
46
+ }
47
+ });
48
+ }
49
+ if (directive.kind === "effect-diagnostics") for (const target of directive.targets) {
50
+ if (!target.startsWith("effect/")) continue;
51
+ findings.push({
52
+ messageId: "effectPrefix",
53
+ data: {
54
+ target,
55
+ bare: target.slice(7)
56
+ }
57
+ });
58
+ }
59
+ if (directive.reason.length === 0) findings.push({
60
+ messageId: "missingReason",
61
+ data: { directive: directive.name }
62
+ });
63
+ return findings;
64
+ };
65
+ const requireSuppressionReason = define({
66
+ name: "require-suppression-reason",
67
+ meta: meta({
68
+ type: "problem",
69
+ description: "Require oxlint, ESLint, Effect diagnostics, and TypeScript suppressions to name what they disable and give a reason after ` -- `.",
70
+ messages: {
71
+ missingTarget: "`{{directive}}` must name what it disables, for example `{{directive}} {{example}} -- reason`. A blanket suppression hides every future finding on the code it covers.",
72
+ missingReason: "`{{directive}}` needs a reason after ` -- ` that says why the check does not apply here, so a reader can tell when the suppression can be removed.",
73
+ effectPrefix: "`@effect-diagnostics` ignores `{{target}}`: @effect/tsgo does not accept the `effect/` prefix. Write the bare rule name, `{{bare}}`."
74
+ }
75
+ }),
76
+ create: function* () {
77
+ const ctx = yield* RuleContext;
78
+ const checkComment = (comment) => Effect.forEach(parseDirectives(comment.value).flatMap(checkDirective), (finding) => ctx.report(fromId({
79
+ node: comment,
80
+ messageId: finding.messageId,
81
+ data: finding.data
82
+ })), { discard: true });
83
+ return { Program: () => Effect.forEach(ctx.sourceCode.getAllComments().filter((comment) => comment.type !== "Shebang"), checkComment, { discard: true }) };
84
+ }
85
+ });
86
+ //#endregion
87
+ export { requireSuppressionReason };
@@ -112,4 +112,4 @@ const extractFixes = (result) => {
112
112
  */
113
113
  const composeFixes = (...fixes) => (fixer) => Arr.flatMap(fixes, (fn) => Array.from(extractFixes(fn(fixer))));
114
114
  //#endregion
115
- export { Diagnostic_exports, fromId, make, replaceText, withFix };
115
+ export { Diagnostic_exports, fromId, make };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-effect",
3
- "version": "0.12.1",
3
+ "version": "0.14.0",
4
4
  "description": "oxlint plugin with Effect-specific lint rules",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,11 +45,12 @@
45
45
  "lint": "concurrently -n ox,effect -c yellow,blue \"oxlint\" \"bun run lint:effect\"",
46
46
  "lint:ox": "oxlint",
47
47
  "lint:effect": "effect-language-service diagnostics --project tsconfig.json --lspconfig \"$(cat .effect-lsp.json)\"",
48
+ "lint:self": "oxlint -c oxlint.self.config.ts src scripts tests",
48
49
  "lint:fix": "oxlint --fix",
49
50
  "fmt": "oxfmt",
50
51
  "fmt:check": "oxfmt --check",
51
52
  "test": "bun run build && bun test",
52
- "gate": "concurrently -n type,style,generated,test -c blue,yellow,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run codegen:check\" \"bun run test\"",
53
+ "gate": "bun run build && concurrently -n type,style,self,generated,test -c blue,yellow,cyan,magenta,green \"bun run typecheck\" \"bun run lint\" \"bun run lint:self\" \"bun run codegen:check\" \"bun test\"",
53
54
  "codegen": "bun run scripts/codegen.ts",
54
55
  "codegen:check": "bun run scripts/codegen.ts --check",
55
56
  "add-rule": "bun run scripts/add-rule.ts",
@@ -61,8 +62,8 @@
61
62
  "@oxlint/plugins": "^1.64.0"
62
63
  },
63
64
  "devDependencies": {
64
- "@changesets/changelog-github": "^0.7.0",
65
- "@changesets/cli": "^2.31.0",
65
+ "@changesets/changelog-github": "^1.0.1",
66
+ "@changesets/cli": "^3.0.3",
66
67
  "@effect/language-service": "latest",
67
68
  "@effect/vitest": "latest",
68
69
  "@types/bun": "latest",