@doubleloop-io/oxlint-plugin-effect 0.0.0-stage → 0.1.0-rc.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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE.anti-slop +21 -0
  3. package/README.md +210 -2
  4. package/RULES.md +911 -0
  5. package/dist/index.d.ts +4 -0
  6. package/dist/index.js +71 -0
  7. package/dist/presets/all.d.ts +7 -0
  8. package/dist/presets/all.js +9 -0
  9. package/dist/presets/all.json +37 -0
  10. package/dist/rules/adapter-import-boundary.d.ts +7 -0
  11. package/dist/rules/adapter-import-boundary.js +103 -0
  12. package/dist/rules/anti-slop-no-manual-effect-error-tag.d.ts +1 -0
  13. package/dist/rules/anti-slop-no-manual-effect-error-tag.js +32 -0
  14. package/dist/rules/anti-slop-no-service-constructor-imports.d.ts +1 -0
  15. package/dist/rules/anti-slop-no-service-constructor-imports.js +49 -0
  16. package/dist/rules/anti-slop-prefer-effect-match.d.ts +1 -0
  17. package/dist/rules/anti-slop-prefer-effect-match.js +45 -0
  18. package/dist/rules/boundary-entrypoint-shape.d.ts +1 -0
  19. package/dist/rules/boundary-entrypoint-shape.js +52 -0
  20. package/dist/rules/error-handling-in-effect-fn-handler.d.ts +1 -0
  21. package/dist/rules/error-handling-in-effect-fn-handler.js +66 -0
  22. package/dist/rules/exported-schema-identifier.d.ts +1 -0
  23. package/dist/rules/exported-schema-identifier.js +117 -0
  24. package/dist/rules/no-asymmetric-match-handlers.d.ts +1 -0
  25. package/dist/rules/no-asymmetric-match-handlers.js +74 -0
  26. package/dist/rules/no-bare-error-in-effect.d.ts +1 -0
  27. package/dist/rules/no-bare-error-in-effect.js +34 -0
  28. package/dist/rules/no-effect-promise.d.ts +1 -0
  29. package/dist/rules/no-effect-promise.js +57 -0
  30. package/dist/rules/no-effect-return-type.d.ts +1 -0
  31. package/dist/rules/no-effect-return-type.js +130 -0
  32. package/dist/rules/no-inline-schema-codec.d.ts +1 -0
  33. package/dist/rules/no-inline-schema-codec.js +46 -0
  34. package/dist/rules/no-inline-yield-star.d.ts +1 -0
  35. package/dist/rules/no-inline-yield-star.js +230 -0
  36. package/dist/rules/no-manual-discriminant-comparison.d.ts +2 -0
  37. package/dist/rules/no-manual-discriminant-comparison.js +58 -0
  38. package/dist/rules/no-manual-discriminant-construction.d.ts +2 -0
  39. package/dist/rules/no-manual-discriminant-construction.js +90 -0
  40. package/dist/rules/no-nested-match.d.ts +1 -0
  41. package/dist/rules/no-nested-match.js +24 -0
  42. package/dist/rules/no-parameterless-catch-all.d.ts +1 -0
  43. package/dist/rules/no-parameterless-catch-all.js +29 -0
  44. package/dist/rules/no-provide-on-definition.d.ts +1 -0
  45. package/dist/rules/no-provide-on-definition.js +94 -0
  46. package/dist/rules/no-transport-types-in-generator.d.ts +1 -0
  47. package/dist/rules/no-transport-types-in-generator.js +63 -0
  48. package/dist/rules/no-yielded-match.d.ts +1 -0
  49. package/dist/rules/no-yielded-match.js +91 -0
  50. package/dist/rules/prefer-effect-fn.d.ts +1 -0
  51. package/dist/rules/prefer-effect-fn.js +102 -0
  52. package/dist/rules/provide-named-layer.d.ts +1 -0
  53. package/dist/rules/provide-named-layer.js +55 -0
  54. package/dist/rules/redacted-secret-names.d.ts +1 -0
  55. package/dist/rules/redacted-secret-names.js +127 -0
  56. package/dist/rules/run-only-at-entry.d.ts +1 -0
  57. package/dist/rules/run-only-at-entry.js +50 -0
  58. package/dist/rules/service-built-with-of.d.ts +1 -0
  59. package/dist/rules/service-built-with-of.js +74 -0
  60. package/dist/rules/service-key-matches-name.d.ts +1 -0
  61. package/dist/rules/service-key-matches-name.js +69 -0
  62. package/dist/rules/service-port-shape-only.d.ts +1 -0
  63. package/dist/rules/service-port-shape-only.js +65 -0
  64. package/dist/rules/single-style-per-function.d.ts +1 -0
  65. package/dist/rules/single-style-per-function.js +77 -0
  66. package/dist/rules/try-object-form.d.ts +1 -0
  67. package/dist/rules/try-object-form.js +26 -0
  68. package/dist/rules/untraced-recursion.d.ts +1 -0
  69. package/dist/rules/untraced-recursion.js +48 -0
  70. package/dist/shared/effect-ast.d.ts +9 -0
  71. package/dist/shared/effect-ast.js +52 -0
  72. package/dist/shared/entry-files.d.ts +6 -0
  73. package/dist/shared/entry-files.js +5 -0
  74. package/dist/shared/file-globs.d.ts +14 -0
  75. package/dist/shared/file-globs.js +17 -0
  76. package/dist/shared/match-ast.d.ts +5 -0
  77. package/dist/shared/match-ast.js +52 -0
  78. package/dist/shared/tagged-values.d.ts +17 -0
  79. package/dist/shared/tagged-values.js +67 -0
  80. package/dist/shared/test-files.d.ts +6 -0
  81. package/dist/shared/test-files.js +10 -0
  82. package/package.json +56 -5
@@ -0,0 +1,17 @@
1
+ import { matchesGlob, relative, sep } from "node:path";
2
+ export const filePatternsSchema = { type: "array", items: { type: "string" }, uniqueItems: true };
3
+ const configuredPatterns = (options, key, defaults) => {
4
+ const [first] = options;
5
+ if (typeof first !== "object" || first === null)
6
+ return defaults;
7
+ const entry = Object.entries(first).find(([name]) => name === key);
8
+ if (entry === undefined)
9
+ return defaults;
10
+ const patterns = entry[1];
11
+ return Array.isArray(patterns) ? patterns.filter((pattern) => typeof pattern === "string") : [];
12
+ };
13
+ // Globs match the path relative to the working directory, with `/` on every platform.
14
+ export const makeFileMatcher = (key, defaults) => (context) => {
15
+ const path = relative(context.cwd, context.filename).split(sep).join("/");
16
+ return configuredPatterns(context.options, key, defaults).some((pattern) => matchesGlob(path, pattern));
17
+ };
@@ -0,0 +1,5 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+ export declare const isMatchCall: (node: ESTree.Node | null | undefined) => node is ESTree.CallExpression;
3
+ export declare const isMatchChain: (node: ESTree.Node | null | undefined) => boolean;
4
+ export declare const handlersOf: (match: ESTree.CallExpression) => ESTree.Expression[];
5
+ export declare const nestedBranches: (handler: ESTree.Node) => ReadonlyArray<ESTree.Node>;
@@ -0,0 +1,52 @@
1
+ import { calleeOf, isFunctionNode, memberOf } from "./effect-ast.js";
2
+ const modulesWithMatch = ["Option", "Result", "Exit", "Either"];
3
+ export const isMatchCall = (node) => {
4
+ if (node?.type !== "CallExpression")
5
+ return false;
6
+ const { callee } = node;
7
+ if (modulesWithMatch.some((module) => memberOf(callee, module) === "match"))
8
+ return true;
9
+ if (memberOf(callee, "Match") === "valueTags")
10
+ return true;
11
+ return (callee.type === "MemberExpression" &&
12
+ !callee.computed &&
13
+ callee.property.type === "Identifier" &&
14
+ callee.property.name === "$match");
15
+ };
16
+ const isMatchStart = (node) => {
17
+ const start = calleeOf(node, "Match");
18
+ return start === "value" || start === "type";
19
+ };
20
+ // `Match.value(x).pipe(...)`, the data-last `Match.type().pipe(...)`, and both through the
21
+ // function form `pipe(Match.value(x), ...)`. Their handlers are arguments of `Match.when`
22
+ // and friends, not object properties, so `handlersOf` sees none.
23
+ export const isMatchChain = (node) => {
24
+ if (node?.type !== "CallExpression")
25
+ return false;
26
+ const { callee } = node;
27
+ if (callee.type === "Identifier")
28
+ return callee.name === "pipe" && isMatchStart(node.arguments[0]);
29
+ return (callee.type === "MemberExpression" &&
30
+ !callee.computed &&
31
+ callee.property.type === "Identifier" &&
32
+ callee.property.name === "pipe" &&
33
+ isMatchStart(callee.object));
34
+ };
35
+ export const handlersOf = (match) => match.arguments.flatMap((argument) => argument.type === "ObjectExpression"
36
+ ? argument.properties.flatMap((property) => property.type === "Property" ? [property.value] : [])
37
+ : []);
38
+ const returnedExpressions = (fn) => {
39
+ if (fn.body?.type !== "BlockStatement")
40
+ return fn.body ? [fn.body] : [];
41
+ return fn.body.body.flatMap((statement) => statement.type === "ReturnStatement" && statement.argument ? [statement.argument] : []);
42
+ };
43
+ // What `no-nested-match` reports in a handler: itself a match, or a returned match or ternary.
44
+ export const nestedBranches = (handler) => {
45
+ if (isMatchCall(handler) || isMatchChain(handler))
46
+ return [handler];
47
+ if (!isFunctionNode(handler))
48
+ return [];
49
+ return returnedExpressions(handler).filter((expression) => isMatchCall(expression) ||
50
+ isMatchChain(expression) ||
51
+ expression.type === "ConditionalExpression");
52
+ };
@@ -0,0 +1,17 @@
1
+ import type { ESTree } from "@oxlint/plugins";
2
+ export declare const isEqualityComparison: (node: ESTree.Node) => node is ESTree.BinaryExpression;
3
+ export declare const isStringLiteral: (node: ESTree.Node | null | undefined) => node is ESTree.StringLiteral;
4
+ export declare const isTagMember: (node: ESTree.Node | null | undefined) => node is ESTree.MemberExpression;
5
+ export declare const isReasonTagMember: (node: ESTree.MemberExpression) => boolean;
6
+ export declare const tagMemberFromComparison: (node: ESTree.BinaryExpression) => ESTree.MemberExpression | undefined;
7
+ export declare const isInsideBroadEffectHandler: (node: ESTree.Node) => boolean;
8
+ export declare const propertyName: (property: ESTree.ObjectProperty) => string | undefined;
9
+ export declare const discriminantsSchema: {
10
+ readonly type: "array";
11
+ readonly items: {
12
+ readonly type: "string";
13
+ };
14
+ readonly uniqueItems: true;
15
+ };
16
+ export declare const configuredDiscriminants: (options: ReadonlyArray<unknown>) => Set<string>;
17
+ export declare const discriminantOf: (node: ESTree.Node | null | undefined, discriminants: ReadonlySet<string>) => string | undefined;
@@ -0,0 +1,67 @@
1
+ import { calleeOf } from "./effect-ast.js";
2
+ const equalityOperators = new Set(["==", "===", "!=", "!=="]);
3
+ const broadEffectCatchMethods = new Set(["catch", "catchAll", "catchIf"]);
4
+ export const isEqualityComparison = (node) => node.type === "BinaryExpression" && equalityOperators.has(node.operator);
5
+ export const isStringLiteral = (node) => node?.type === "Literal" && typeof node.value === "string";
6
+ const isPropertyNamed = (node, name) => node.computed
7
+ ? isStringLiteral(node.property) && node.property.value === name
8
+ : node.property.type === "Identifier" && node.property.name === name;
9
+ export const isTagMember = (node) => node?.type === "MemberExpression" && isPropertyNamed(node, "_tag");
10
+ export const isReasonTagMember = (node) => node.object.type === "MemberExpression" && isPropertyNamed(node.object, "reason");
11
+ export const tagMemberFromComparison = (node) => {
12
+ if (!isEqualityComparison(node))
13
+ return undefined;
14
+ if (isTagMember(node.left) && isStringLiteral(node.right))
15
+ return node.left;
16
+ if (isTagMember(node.right) && isStringLiteral(node.left))
17
+ return node.right;
18
+ return undefined;
19
+ };
20
+ // Only the nearest function counts: a `_tag` check in a callback nested inside the handler
21
+ // is not the handler discriminating its error.
22
+ export const isInsideBroadEffectHandler = (node) => {
23
+ for (let current = node.parent; current; current = current.parent) {
24
+ if (current.type === "ArrowFunctionExpression" || current.type === "FunctionExpression") {
25
+ const call = current.parent;
26
+ return (call?.type === "CallExpression" &&
27
+ broadEffectCatchMethods.has(calleeOf(call, "Effect") ?? "") &&
28
+ call.arguments.some((argument) => argument === current));
29
+ }
30
+ }
31
+ return false;
32
+ };
33
+ export const propertyName = (property) => {
34
+ if (!property.computed && property.key.type === "Identifier")
35
+ return property.key.name;
36
+ if (isStringLiteral(property.key))
37
+ return property.key.value;
38
+ return undefined;
39
+ };
40
+ // The fields that discriminate a closed union: `_tag` for Effect's tagged values, plus whatever
41
+ // the codebase uses (`type`, `kind`). Shared by the rules on comparison and construction.
42
+ const defaultDiscriminants = ["_tag"];
43
+ export const discriminantsSchema = {
44
+ type: "array",
45
+ items: { type: "string" },
46
+ uniqueItems: true,
47
+ };
48
+ export const configuredDiscriminants = (options) => {
49
+ const [first] = options;
50
+ if (typeof first !== "object" || first === null || !("discriminants" in first)) {
51
+ return new Set(defaultDiscriminants);
52
+ }
53
+ const names = first.discriminants;
54
+ return new Set(Array.isArray(names) ? names.filter((name) => typeof name === "string") : []);
55
+ };
56
+ export const discriminantOf = (node, discriminants) => {
57
+ if (node?.type !== "MemberExpression")
58
+ return undefined;
59
+ const name = node.computed
60
+ ? isStringLiteral(node.property)
61
+ ? node.property.value
62
+ : undefined
63
+ : node.property.type === "Identifier"
64
+ ? node.property.name
65
+ : undefined;
66
+ return name !== undefined && discriminants.has(name) ? name : undefined;
67
+ };
@@ -0,0 +1,6 @@
1
+ export declare const defaultTestFiles: string[];
2
+ export declare const isTestFile: (context: {
3
+ readonly filename: string;
4
+ readonly cwd: string;
5
+ readonly options: ReadonlyArray<unknown>;
6
+ }) => boolean;
@@ -0,0 +1,10 @@
1
+ import { makeFileMatcher } from "./file-globs.js";
2
+ // `src/testing/` helpers are not matched on purpose: they are support code, not tests.
3
+ export const defaultTestFiles = [
4
+ "**/*.test.*",
5
+ "**/*.spec.*",
6
+ "**/__tests__/**",
7
+ "**/test/**",
8
+ "**/tests/**",
9
+ ];
10
+ export const isTestFile = makeFileMatcher("testFiles", defaultTestFiles);
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
- "name": "@doubleloop-io/oxlint-plugin-effect",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@doubleloop-io/oxlint-plugin-effect",
3
+ "version": "0.1.0-rc.0",
4
+ "description": "Oxlint rules for Effect 4 code",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/doubleloop-io/oxlint-plugin-effect.git"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "RULES.md",
13
+ "README.md",
14
+ "LICENSE",
15
+ "LICENSE.anti-slop"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ "./presets/all": {
20
+ "types": "./dist/presets/all.d.ts",
21
+ "default": "./dist/presets/all.js"
22
+ },
23
+ "./presets/all.json": "./dist/presets/all.json"
24
+ },
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
28
+ "scripts": {
29
+ "format": "oxfmt",
30
+ "format:check": "oxfmt --check",
31
+ "typecheck": "tsc --noEmit",
32
+ "lint": "oxlint --deny-warnings",
33
+ "test": "node --test install-skill.test.ts rules-md.test.ts rules/*.test.ts",
34
+ "check": "pnpm format:check && pnpm typecheck && pnpm lint && pnpm test",
35
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node presets/all-json.ts",
36
+ "pack:check": "node --test presets/all-dist.test.ts && node scripts/pack-check.ts",
37
+ "build-and-pack": "pnpm check && pnpm build && pnpm pack:check && pnpm smoke-test",
38
+ "smoke-test": "node scripts/smoke-test.ts"
39
+ },
40
+ "dependencies": {
41
+ "@oxlint/plugins": "^1.82.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^24.13.6",
45
+ "oxfmt": "0.67.0",
46
+ "oxlint": "1.82.0",
47
+ "publint": "^0.3.25",
48
+ "typescript": "^7.0.2"
49
+ },
50
+ "peerDependencies": {
51
+ "oxlint": "^1.82.0"
52
+ },
53
+ "engines": {
54
+ "node": "^22.20.0 || >=24.8.0"
55
+ },
56
+ "packageManager": "pnpm@12.4.1"
57
+ }