@thethracian/oxlint-config 0.2.1 → 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 (279) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +124 -18
  3. package/dist/codemod-fix/cli.d.ts +3 -0
  4. package/dist/codemod-fix/cli.d.ts.map +1 -0
  5. package/dist/codemod-fix/cli.js +22 -0
  6. package/dist/codemod-fix/cli.js.map +1 -0
  7. package/dist/codemod-fix/index.d.ts +44 -0
  8. package/dist/codemod-fix/index.d.ts.map +1 -0
  9. package/dist/codemod-fix/index.js +110 -0
  10. package/dist/codemod-fix/index.js.map +1 -0
  11. package/dist/codemods/arrow-body-style.d.ts +7 -0
  12. package/dist/codemods/arrow-body-style.d.ts.map +1 -0
  13. package/dist/codemods/arrow-body-style.js +106 -0
  14. package/dist/codemods/arrow-body-style.js.map +1 -0
  15. package/dist/codemods/comment-format.d.ts +24 -0
  16. package/dist/codemods/comment-format.d.ts.map +1 -0
  17. package/dist/codemods/comment-format.js +100 -0
  18. package/dist/codemods/comment-format.js.map +1 -0
  19. package/dist/codemods/explicit-return-types.d.ts +7 -0
  20. package/dist/codemods/explicit-return-types.d.ts.map +1 -0
  21. package/dist/codemods/explicit-return-types.js +350 -0
  22. package/dist/codemods/explicit-return-types.js.map +1 -0
  23. package/dist/codemods/format-file-header.d.ts +7 -0
  24. package/dist/codemods/format-file-header.d.ts.map +1 -0
  25. package/dist/codemods/format-file-header.js +163 -0
  26. package/dist/codemods/format-file-header.js.map +1 -0
  27. package/dist/codemods/format-jsdoc-comments.d.ts +7 -0
  28. package/dist/codemods/format-jsdoc-comments.d.ts.map +1 -0
  29. package/dist/codemods/format-jsdoc-comments.js +129 -0
  30. package/dist/codemods/format-jsdoc-comments.js.map +1 -0
  31. package/dist/codemods/function-declarations.d.ts +7 -0
  32. package/dist/codemods/function-declarations.d.ts.map +1 -0
  33. package/dist/codemods/function-declarations.js +202 -0
  34. package/dist/codemods/function-declarations.js.map +1 -0
  35. package/dist/codemods/inline-export-lists.d.ts +7 -0
  36. package/dist/codemods/inline-export-lists.d.ts.map +1 -0
  37. package/dist/codemods/inline-export-lists.js +168 -0
  38. package/dist/codemods/inline-export-lists.js.map +1 -0
  39. package/dist/codemods/internal-export-docs.d.ts +7 -0
  40. package/dist/codemods/internal-export-docs.d.ts.map +1 -0
  41. package/dist/codemods/internal-export-docs.js +93 -0
  42. package/dist/codemods/internal-export-docs.js.map +1 -0
  43. package/dist/codemods/no-ternary-branch-initializers.d.ts +14 -0
  44. package/dist/codemods/no-ternary-branch-initializers.d.ts.map +1 -0
  45. package/dist/codemods/no-ternary-branch-initializers.js +240 -0
  46. package/dist/codemods/no-ternary-branch-initializers.js.map +1 -0
  47. package/dist/codemods/no-ternary-variable-initializers.d.ts +14 -0
  48. package/dist/codemods/no-ternary-variable-initializers.d.ts.map +1 -0
  49. package/dist/codemods/no-ternary-variable-initializers.js +235 -0
  50. package/dist/codemods/no-ternary-variable-initializers.js.map +1 -0
  51. package/dist/codemods/no-ternary.d.ts +7 -0
  52. package/dist/codemods/no-ternary.d.ts.map +1 -0
  53. package/dist/codemods/no-ternary.js +215 -0
  54. package/dist/codemods/no-ternary.js.map +1 -0
  55. package/dist/codemods/rename-acronyms.d.ts +8 -0
  56. package/dist/codemods/rename-acronyms.d.ts.map +1 -0
  57. package/dist/codemods/rename-acronyms.js +193 -0
  58. package/dist/codemods/rename-acronyms.js.map +1 -0
  59. package/dist/codemods/run.d.ts +2 -0
  60. package/dist/codemods/run.d.ts.map +1 -0
  61. package/dist/codemods/run.js +14 -0
  62. package/dist/codemods/run.js.map +1 -0
  63. package/dist/codemods/sort-imports.d.ts +7 -0
  64. package/dist/codemods/sort-imports.d.ts.map +1 -0
  65. package/dist/codemods/sort-imports.js +235 -0
  66. package/dist/codemods/sort-imports.js.map +1 -0
  67. package/dist/index.d.ts +30 -96
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +156 -94
  70. package/dist/index.js.map +1 -1
  71. package/dist/rules/acronym-case.d.ts +7 -3
  72. package/dist/rules/acronym-case.d.ts.map +1 -1
  73. package/dist/rules/acronym-case.js +129 -118
  74. package/dist/rules/acronym-case.js.map +1 -1
  75. package/dist/rules/acronyms-1.d.ts +3 -0
  76. package/dist/rules/acronyms-1.d.ts.map +1 -0
  77. package/dist/rules/acronyms-1.js +407 -0
  78. package/dist/rules/acronyms-1.js.map +1 -0
  79. package/dist/rules/acronyms-2.d.ts +3 -0
  80. package/dist/rules/acronyms-2.d.ts.map +1 -0
  81. package/dist/rules/acronyms-2.js +407 -0
  82. package/dist/rules/acronyms-2.js.map +1 -0
  83. package/dist/rules/acronyms-3.d.ts +3 -0
  84. package/dist/rules/acronyms-3.d.ts.map +1 -0
  85. package/dist/rules/acronyms-3.js +407 -0
  86. package/dist/rules/acronyms-3.js.map +1 -0
  87. package/dist/rules/acronyms-4.d.ts +3 -0
  88. package/dist/rules/acronyms-4.d.ts.map +1 -0
  89. package/dist/rules/acronyms-4.js +377 -0
  90. package/dist/rules/acronyms-4.js.map +1 -0
  91. package/dist/rules/acronyms.d.ts +1 -8
  92. package/dist/rules/acronyms.d.ts.map +1 -1
  93. package/dist/rules/acronyms.js +14 -1578
  94. package/dist/rules/acronyms.js.map +1 -1
  95. package/dist/rules/boolean-prefix.d.ts +11 -2
  96. package/dist/rules/boolean-prefix.d.ts.map +1 -1
  97. package/dist/rules/boolean-prefix.js +95 -59
  98. package/dist/rules/boolean-prefix.js.map +1 -1
  99. package/dist/rules/camel-case-identifiers.d.ts +3 -4
  100. package/dist/rules/camel-case-identifiers.d.ts.map +1 -1
  101. package/dist/rules/camel-case-identifiers.js +59 -40
  102. package/dist/rules/camel-case-identifiers.js.map +1 -1
  103. package/dist/rules/char-class.d.ts +26 -13
  104. package/dist/rules/char-class.d.ts.map +1 -1
  105. package/dist/rules/char-class.js +56 -24
  106. package/dist/rules/char-class.js.map +1 -1
  107. package/dist/rules/diagnostic-guidance.d.ts +91 -0
  108. package/dist/rules/diagnostic-guidance.d.ts.map +1 -0
  109. package/dist/rules/diagnostic-guidance.js +243 -0
  110. package/dist/rules/diagnostic-guidance.js.map +1 -0
  111. package/dist/rules/effect-default-ast.d.ts +89 -0
  112. package/dist/rules/effect-default-ast.d.ts.map +1 -0
  113. package/dist/rules/effect-default-ast.js +208 -0
  114. package/dist/rules/effect-default-ast.js.map +1 -0
  115. package/dist/rules/effect-default-compat-rules.d.ts +76 -0
  116. package/dist/rules/effect-default-compat-rules.d.ts.map +1 -0
  117. package/dist/rules/effect-default-compat-rules.js +270 -0
  118. package/dist/rules/effect-default-compat-rules.js.map +1 -0
  119. package/dist/rules/effect-default-env-rules.d.ts +60 -0
  120. package/dist/rules/effect-default-env-rules.d.ts.map +1 -0
  121. package/dist/rules/effect-default-env-rules.js +259 -0
  122. package/dist/rules/effect-default-env-rules.js.map +1 -0
  123. package/dist/rules/effect-default-fiber-helpers.d.ts +13 -0
  124. package/dist/rules/effect-default-fiber-helpers.d.ts.map +1 -0
  125. package/dist/rules/effect-default-fiber-helpers.js +76 -0
  126. package/dist/rules/effect-default-fiber-helpers.js.map +1 -0
  127. package/dist/rules/effect-default-floating-helpers.d.ts +7 -0
  128. package/dist/rules/effect-default-floating-helpers.d.ts.map +1 -0
  129. package/dist/rules/effect-default-floating-helpers.js +113 -0
  130. package/dist/rules/effect-default-floating-helpers.js.map +1 -0
  131. package/dist/rules/effect-default-helpers.d.ts +8 -32
  132. package/dist/rules/effect-default-helpers.d.ts.map +1 -1
  133. package/dist/rules/effect-default-helpers.js +11 -535
  134. package/dist/rules/effect-default-helpers.js.map +1 -1
  135. package/dist/rules/effect-default-resource-helpers.d.ts +18 -4
  136. package/dist/rules/effect-default-resource-helpers.d.ts.map +1 -1
  137. package/dist/rules/effect-default-resource-helpers.js +30 -13
  138. package/dist/rules/effect-default-resource-helpers.js.map +1 -1
  139. package/dist/rules/effect-default-safety-helpers.d.ts +43 -0
  140. package/dist/rules/effect-default-safety-helpers.d.ts.map +1 -0
  141. package/dist/rules/effect-default-safety-helpers.js +126 -0
  142. package/dist/rules/effect-default-safety-helpers.js.map +1 -0
  143. package/dist/rules/effect-default-scan-helpers.d.ts +43 -0
  144. package/dist/rules/effect-default-scan-helpers.d.ts.map +1 -0
  145. package/dist/rules/effect-default-scan-helpers.js +141 -0
  146. package/dist/rules/effect-default-scan-helpers.js.map +1 -0
  147. package/dist/rules/effect-default-schema-helpers.d.ts +37 -0
  148. package/dist/rules/effect-default-schema-helpers.d.ts.map +1 -0
  149. package/dist/rules/effect-default-schema-helpers.js +104 -0
  150. package/dist/rules/effect-default-schema-helpers.js.map +1 -0
  151. package/dist/rules/effect-default-test-helpers.d.ts +18 -4
  152. package/dist/rules/effect-default-test-helpers.d.ts.map +1 -1
  153. package/dist/rules/effect-default-test-helpers.js +41 -20
  154. package/dist/rules/effect-default-test-helpers.js.map +1 -1
  155. package/dist/rules/effect-default-workflow-helpers.d.ts +91 -0
  156. package/dist/rules/effect-default-workflow-helpers.d.ts.map +1 -0
  157. package/dist/rules/effect-default-workflow-helpers.js +247 -0
  158. package/dist/rules/effect-default-workflow-helpers.js.map +1 -0
  159. package/dist/rules/effect-default.d.ts +1 -1
  160. package/dist/rules/effect-default.d.ts.map +1 -1
  161. package/dist/rules/effect-default.js +105 -680
  162. package/dist/rules/effect-default.js.map +1 -1
  163. package/dist/rules/effect-exported-declarations.d.ts +19 -0
  164. package/dist/rules/effect-exported-declarations.d.ts.map +1 -0
  165. package/dist/rules/effect-exported-declarations.js +237 -0
  166. package/dist/rules/effect-exported-declarations.js.map +1 -0
  167. package/dist/rules/effect-path-options.d.ts +49 -11
  168. package/dist/rules/effect-path-options.d.ts.map +1 -1
  169. package/dist/rules/effect-path-options.js +85 -52
  170. package/dist/rules/effect-path-options.js.map +1 -1
  171. package/dist/rules/effect-rule-aliases.d.ts +55 -0
  172. package/dist/rules/effect-rule-aliases.d.ts.map +1 -0
  173. package/dist/rules/effect-rule-aliases.js +278 -0
  174. package/dist/rules/effect-rule-aliases.js.map +1 -0
  175. package/dist/rules/effect-rule-core.d.ts +38 -24
  176. package/dist/rules/effect-rule-core.d.ts.map +1 -1
  177. package/dist/rules/effect-rule-core.js +157 -344
  178. package/dist/rules/effect-rule-core.js.map +1 -1
  179. package/dist/rules/effect-rule-names.d.ts +12 -3
  180. package/dist/rules/effect-rule-names.d.ts.map +1 -1
  181. package/dist/rules/effect-rule-names.js +15 -3
  182. package/dist/rules/effect-rule-names.js.map +1 -1
  183. package/dist/rules/effect-source-comments.d.ts +7 -0
  184. package/dist/rules/effect-source-comments.d.ts.map +1 -0
  185. package/dist/rules/effect-source-comments.js +227 -0
  186. package/dist/rules/effect-source-comments.js.map +1 -0
  187. package/dist/rules/effect-source-helpers.d.ts +4 -9
  188. package/dist/rules/effect-source-helpers.d.ts.map +1 -1
  189. package/dist/rules/effect-source-helpers.js +7 -358
  190. package/dist/rules/effect-source-helpers.js.map +1 -1
  191. package/dist/rules/effect-source-navigation.d.ts +25 -0
  192. package/dist/rules/effect-source-navigation.d.ts.map +1 -0
  193. package/dist/rules/effect-source-navigation.js +244 -0
  194. package/dist/rules/effect-source-navigation.js.map +1 -0
  195. package/dist/rules/effect-source-scan.d.ts +30 -7
  196. package/dist/rules/effect-source-scan.d.ts.map +1 -1
  197. package/dist/rules/effect-source-scan.js +372 -300
  198. package/dist/rules/effect-source-scan.js.map +1 -1
  199. package/dist/rules/effect-strict-ast-specs.d.ts +8 -0
  200. package/dist/rules/effect-strict-ast-specs.d.ts.map +1 -0
  201. package/dist/rules/effect-strict-ast-specs.js +166 -0
  202. package/dist/rules/effect-strict-ast-specs.js.map +1 -0
  203. package/dist/rules/effect-strict-boundary-helpers.d.ts +25 -0
  204. package/dist/rules/effect-strict-boundary-helpers.d.ts.map +1 -0
  205. package/dist/rules/effect-strict-boundary-helpers.js +127 -0
  206. package/dist/rules/effect-strict-boundary-helpers.js.map +1 -0
  207. package/dist/rules/effect-strict-core-specs.d.ts +8 -0
  208. package/dist/rules/effect-strict-core-specs.d.ts.map +1 -0
  209. package/dist/rules/effect-strict-core-specs.js +334 -0
  210. package/dist/rules/effect-strict-core-specs.js.map +1 -0
  211. package/dist/rules/effect-strict-external-helpers.d.ts +25 -0
  212. package/dist/rules/effect-strict-external-helpers.d.ts.map +1 -0
  213. package/dist/rules/effect-strict-external-helpers.js +87 -0
  214. package/dist/rules/effect-strict-external-helpers.js.map +1 -0
  215. package/dist/rules/effect-strict-helpers.d.ts +75 -33
  216. package/dist/rules/effect-strict-helpers.d.ts.map +1 -1
  217. package/dist/rules/effect-strict-helpers.js +93 -384
  218. package/dist/rules/effect-strict-helpers.js.map +1 -1
  219. package/dist/rules/effect-strict-internals.d.ts +198 -0
  220. package/dist/rules/effect-strict-internals.d.ts.map +1 -0
  221. package/dist/rules/effect-strict-internals.js +477 -0
  222. package/dist/rules/effect-strict-internals.js.map +1 -0
  223. package/dist/rules/effect-strict-segment-helpers.d.ts +79 -0
  224. package/dist/rules/effect-strict-segment-helpers.d.ts.map +1 -0
  225. package/dist/rules/effect-strict-segment-helpers.js +308 -0
  226. package/dist/rules/effect-strict-segment-helpers.js.map +1 -0
  227. package/dist/rules/effect-strict.d.ts +1 -1
  228. package/dist/rules/effect-strict.d.ts.map +1 -1
  229. package/dist/rules/effect-strict.js +10 -725
  230. package/dist/rules/effect-strict.js.map +1 -1
  231. package/dist/rules/max-import-depth.d.ts +2 -2
  232. package/dist/rules/max-import-depth.d.ts.map +1 -1
  233. package/dist/rules/max-import-depth.js +52 -17
  234. package/dist/rules/max-import-depth.js.map +1 -1
  235. package/dist/rules/max-line-length.d.ts +9 -2
  236. package/dist/rules/max-line-length.d.ts.map +1 -1
  237. package/dist/rules/max-line-length.js +52 -15
  238. package/dist/rules/max-line-length.js.map +1 -1
  239. package/dist/rules/no-commented-out-code.d.ts +3 -7
  240. package/dist/rules/no-commented-out-code.d.ts.map +1 -1
  241. package/dist/rules/no-commented-out-code.js +157 -102
  242. package/dist/rules/no-commented-out-code.js.map +1 -1
  243. package/dist/rules/pascal-case-types.d.ts +1 -1
  244. package/dist/rules/pascal-case-types.d.ts.map +1 -1
  245. package/dist/rules/pascal-case-types.js +65 -46
  246. package/dist/rules/pascal-case-types.js.map +1 -1
  247. package/dist/rules/plugin-commented-out-code-rule.d.ts +32 -0
  248. package/dist/rules/plugin-commented-out-code-rule.d.ts.map +1 -0
  249. package/dist/rules/plugin-commented-out-code-rule.js +135 -0
  250. package/dist/rules/plugin-commented-out-code-rule.js.map +1 -0
  251. package/dist/rules/plugin.d.ts +1 -0
  252. package/dist/rules/plugin.d.ts.map +1 -1
  253. package/dist/rules/plugin.js +164 -154
  254. package/dist/rules/plugin.js.map +1 -1
  255. package/dist/rules/private-underscore.d.ts +1 -1
  256. package/dist/rules/private-underscore.d.ts.map +1 -1
  257. package/dist/rules/private-underscore.js +11 -4
  258. package/dist/rules/private-underscore.js.map +1 -1
  259. package/dist/rules/require-file-doc.d.ts +5 -6
  260. package/dist/rules/require-file-doc.d.ts.map +1 -1
  261. package/dist/rules/require-file-doc.js +127 -78
  262. package/dist/rules/require-file-doc.js.map +1 -1
  263. package/dist/rules/require-function-doc-ignored-text.d.ts +7 -0
  264. package/dist/rules/require-function-doc-ignored-text.d.ts.map +1 -0
  265. package/dist/rules/require-function-doc-ignored-text.js +67 -0
  266. package/dist/rules/require-function-doc-ignored-text.js.map +1 -0
  267. package/dist/rules/require-function-doc-local-exports.d.ts +7 -0
  268. package/dist/rules/require-function-doc-local-exports.d.ts.map +1 -0
  269. package/dist/rules/require-function-doc-local-exports.js +114 -0
  270. package/dist/rules/require-function-doc-local-exports.js.map +1 -0
  271. package/dist/rules/require-function-doc.d.ts +3 -18
  272. package/dist/rules/require-function-doc.d.ts.map +1 -1
  273. package/dist/rules/require-function-doc.js +396 -221
  274. package/dist/rules/require-function-doc.js.map +1 -1
  275. package/dist/rules/source-cache.d.ts +13 -5
  276. package/dist/rules/source-cache.d.ts.map +1 -1
  277. package/dist/rules/source-cache.js +35 -22
  278. package/dist/rules/source-cache.js.map +1 -1
  279. package/package.json +10 -2
@@ -1,9 +1,13 @@
1
- import { hasBroadCatchAllWithoutRethrow, hasEffectInArrayForEach, hasEffectInPromiseCallback, hasForkDaemonWithoutCleanup, hasForkInUninterruptibleWithoutRestore, hasErrorMappingWithoutCause, hasCastAfterSchemaDecode, hasExternalJsonWithoutDecodeUnknown, hasFloatingEffect, hasJsonParsedBeforeSchemaStringDecode, hasMultipleCatchTagsInOnePipe, hasNestedFlatMap, hasParsedJsonNumberFromString, hasRecursiveEffectWithoutSuspend, hasReturnEffectInGen, hasRunForkWithoutObserver, hasRuntimeInEffect, hasSchemaPromiseDecode, hasSchemaSyncDecodeInEffectWorkflow, hasSyncForPromise, hasSyncForThrowingOps, hasTryPromiseWithoutTypedCatch, hasUnhandledSchemaEffectDecode, hasUnboundedEffectConcurrency, hasUnboundedFlatMapConcurrency, hasUnloggedIgnore, hasUnobservedFork, hasAsyncAwaitInEffect, hasThrowInEffect, hasUnsafeLazyEvaluation, hasYieldWithoutStarInGen, } from './effect-default-helpers.js';
2
- import { hasUnreleasedAcquire, hasUnscopedAcquireRelease, hasUnscopedResourceWorkflow, } from './effect-default-resource-helpers.js';
3
- import { hasForkBeforeTestClockAdjust, hasRealSleepWithoutTestClock, hasTestClockWithoutEffectContext, } from './effect-default-test-helpers.js';
4
- import { isConfiguredPath, isEffectTestPath, strictPathOptionsSchema, } from './effect-path-options.js';
5
- import { effectImportAliases, effectFunctionAliases, hasEffectSignal, hasRuntimeCall, makeRules, } from './effect-rule-core.js';
6
- import { exportedCallableDeclarationSegments, exportedDeclarationSegments, findBalancedCallEnd, stripCommentsAndStrings, } from './effect-source-helpers.js';
1
+ /* -------------------------------------------------------------------------- */
2
+ /* Always-on custom Effect lint rule definitions. */
3
+ /* -------------------------------------------------------------------------- */
4
+ import { arrayValue, effectCallPredicate, identifierName, isStringLikeLiteral, memberParts, nodeType, objectValue, propertyKeyName, reportAST, } from './effect-default-ast.js';
5
+ import { exportedCallableDeclarationSegments, exportedDeclarationSegments, stripCommentsAndStrings, } from './effect-source-helpers.js';
6
+ import { hasAsyncAwaitInEffect, hasBroadCatchAllWithoutRethrow, hasEffectInArrayForEach, hasEffectInPromiseCallback, hasErrorMappingWithoutCause, hasFloatingEffect, hasMultipleCatchTagsInOnePipe, hasNestedFlatMap, hasRecursiveEffectWithoutSuspend, hasReturnEffectInGen, hasRunForkWithoutObserver, hasRuntimeInEffect, hasSyncForPromise, hasSyncForThrowingOPS, hasThrowInEffect, hasTryPromiseWithoutTypedCatch, hasUnloggedIgnore, hasUnobservedFork, hasUnsafeLazyEvaluation, hasYieldWithoutStarInGen, } from './effect-default-helpers.js';
7
+ import { hasEffectSignal, makeRules } from './effect-rule-core.js';
8
+ import { effectDefaultCompatibilitySpecs } from './effect-default-compat-rules.js';
9
+ import { effectDefaultEnvironmentSpecs } from './effect-default-env-rules.js';
10
+ import { strictPathOptionsSchema } from './effect-path-options.js';
7
11
  const effectDefaultRuleTokens = [
8
12
  'Effect',
9
13
  'Schema',
@@ -22,130 +26,11 @@ const effectDefaultRuleTokens = [
22
26
  'JSON.parse',
23
27
  'response.json',
24
28
  ];
25
- function reportAst(context, message, node) {
26
- context.report({ message, node });
27
- }
28
- function nodeType(node) {
29
- return typeof node === 'object' && node !== null ? node.type : undefined;
30
- }
31
- function identifierName(node) {
32
- return nodeType(node) === 'Identifier' ? node.name : undefined;
33
- }
34
- function literalValue(node) {
35
- return nodeType(node) === 'Literal' ? node.value : undefined;
36
- }
37
- function isStringLikeLiteral(node) {
38
- if (typeof literalValue(node) === 'string') {
39
- return true;
40
- }
41
- if (nodeType(node) !== 'TemplateLiteral') {
42
- return false;
43
- }
44
- const { expressions } = node;
45
- return expressions?.length === 0;
46
- }
47
- function memberParts(node) {
48
- if (nodeType(node) !== 'MemberExpression') {
49
- return {};
50
- }
51
- const member = node;
52
- return {
53
- objectName: identifierName(member.object),
54
- propertyName: identifierName(member.property),
55
- };
56
- }
57
- function effectCallPredicate(source, names) {
58
- const memberNames = new Set(names);
59
- const importAliases = new Set(effectImportAliases(source));
60
- const functionAliases = new Set(names.flatMap((name) => effectFunctionAliases(source, 'Effect', name)));
61
- return (callee) => {
62
- const { objectName, propertyName } = memberParts(callee);
63
- if (objectName && propertyName) {
64
- return importAliases.has(objectName) && memberNames.has(propertyName);
65
- }
66
- const calleeName = identifierName(callee);
67
- return Boolean(calleeName && functionAliases.has(calleeName));
68
- };
69
- }
70
- function propertyKeyName(node) {
71
- const value = literalValue(node);
72
- return identifierName(node) ?? (typeof value === 'string' ? value : undefined);
73
- }
74
- function typeReferenceName(node) {
75
- if (nodeType(node) !== 'TSTypeReference') {
76
- return undefined;
77
- }
78
- const { typeName } = node;
79
- if (nodeType(typeName) === 'Identifier') {
80
- return identifierName(typeName);
81
- }
82
- if (nodeType(typeName) !== 'TSQualifiedName') {
83
- return undefined;
84
- }
85
- const qualified = typeName;
86
- const leftName = identifierName(qualified.left);
87
- const rightName = identifierName(qualified.right);
88
- return leftName && rightName ? `${leftName}.${rightName}` : undefined;
89
- }
90
- function firstTypeArgumentName(node) {
91
- const params = node.typeArguments?.params;
92
- return typeReferenceName(params?.[0]);
93
- }
94
- function effectServiceSelfName(superClass, source) {
95
- if (nodeType(superClass) !== 'CallExpression') {
96
- return undefined;
97
- }
98
- const outer = superClass;
99
- const outerSelf = typeReferenceName(outer.typeArguments?.params?.[0]);
100
- const inner = outer.callee;
101
- if (nodeType(inner) !== 'CallExpression') {
102
- return undefined;
103
- }
104
- const innerCall = inner;
105
- const { objectName, propertyName } = memberParts(innerCall.callee);
106
- if (objectName === 'Context' && propertyName === 'Tag') {
107
- return outerSelf;
108
- }
109
- if (objectName &&
110
- propertyName === 'Service' &&
111
- effectImportAliases(source).includes(objectName)) {
112
- return firstTypeArgumentName(inner);
113
- }
114
- return undefined;
115
- }
116
- function hasMixedEffectImportStyles(source) {
117
- const hasNamedRootEffectImport = /import\s*{[^}]*\bEffect\b[^}]*}\s*from\s*['"]effect['"]/.test(source);
118
- const hasNamespaceEffectImport = /import\s+\*\s+as\s+[A-Za-z_$][\w$]*\s+from\s*['"]effect(?:\/Effect)?['"]/.test(source);
119
- return hasNamedRootEffectImport && hasNamespaceEffectImport;
120
- }
121
- function escapeRegExp(value) {
122
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
123
- }
124
- function hasTryCatchInEffectGen(source) {
125
- const code = stripCommentsAndStrings(source);
126
- const aliases = effectImportAliases(source).map(escapeRegExp).join('|');
127
- if (!aliases) {
128
- return false;
129
- }
130
- for (const match of code.matchAll(new RegExp(`\\b(?:${aliases})\\.gen\\s*\\(`, 'g'))) {
131
- const openParenIndex = code.indexOf('(', match.index);
132
- if (openParenIndex === -1) {
133
- continue;
134
- }
135
- const bodyStart = openParenIndex + 1;
136
- const body = code.slice(bodyStart, findBalancedCallEnd(code, openParenIndex));
137
- const tryCatchMatch = /\btry\s*{[\s\S]*?\bcatch\s*\(/.exec(body);
138
- if (tryCatchMatch?.index !== undefined) {
139
- return bodyStart + tryCatchMatch.index;
140
- }
141
- }
142
- return false;
143
- }
144
- function hasStringErrorFailure(source) {
29
+ const hasStringErrorFailure = (source) => {
145
30
  const match = /\bEffect\.fail\s*\(\s*["'`]/.exec(stripCommentsAndStrings(source));
146
31
  return match?.index ?? false;
147
- }
148
- function hasUntaggedErrorFailure(source) {
32
+ };
33
+ const hasUntaggedErrorFailure = (source) => {
149
34
  const code = stripCommentsAndStrings(source);
150
35
  const nativeErrorMatch = /\bEffect\.fail\s*\(\s*new\s+Error\s*\(/.exec(code);
151
36
  if (nativeErrorMatch) {
@@ -153,124 +38,94 @@ function hasUntaggedErrorFailure(source) {
153
38
  }
154
39
  const objectErrorMatch = /\bEffect\.fail\s*\(\s*{(?![^}]*\b_tag\s*:)/.exec(code);
155
40
  return objectErrorMatch?.index ?? false;
156
- }
157
- function hasNativeErrorClassInEffectModule(source) {
158
- if (!hasEffectSignal(source)) {
159
- return false;
160
- }
161
- const match = /\bclass\s+[A-Z][\w$]*\s+extends\s+Error\b/.exec(stripCommentsAndStrings(source));
162
- return match?.index ?? false;
163
- }
164
- function hasUnsafeEffectTypeAssertion(source) {
165
- const code = stripCommentsAndStrings(source);
166
- const match = /\bas\s+Effect\.Effect\s*<[^>]*>/.exec(code);
167
- return match?.index ?? false;
168
- }
169
- function hasServiceSelfMismatch(source) {
170
- const code = stripCommentsAndStrings(source);
171
- const legacyPattern = /\bclass\s+([A-Z][\w$]*)\s+extends\s+(?:Context\.Tag|Effect\.Service|Effect\.Tag)\s*\([^)]*\)\s*<\s*([A-Z][\w$]*)\b/g;
172
- for (const match of code.matchAll(legacyPattern)) {
173
- const [, className, selfName] = match;
174
- if (className !== selfName) {
175
- return match.index;
176
- }
177
- }
178
- const servicePattern = /\bclass\s+([A-Z][\w$]*)\s+extends\s+Effect\.Service\s*<\s*([A-Z][\w$]*)\s*>\s*\(\s*\)/g;
179
- for (const match of code.matchAll(servicePattern)) {
180
- const [, className, selfName] = match;
181
- if (className !== selfName) {
182
- return match.index;
183
- }
41
+ };
42
+ const isUntaggedErrorFailureArgument = (firstArg) => {
43
+ if (nodeType(firstArg) === 'NewExpression' &&
44
+ identifierName(objectValue(firstArg, 'callee')) === 'Error') {
45
+ return true;
184
46
  }
185
- return false;
186
- }
187
- function hasEffectFnIife(source) {
188
- const code = stripCommentsAndStrings(source);
189
- const match = /\bEffect\.fn(?:Untraced|UntracedEager)?\s*\([\s\S]*?\)\s*\([\s\S]*?\)\s*\(/.exec(code);
190
- return match?.index ?? false;
191
- }
47
+ return (nodeType(firstArg) === 'ObjectExpression' &&
48
+ !arrayValue(objectValue(firstArg, 'properties')).some((property) => propertyKeyName(objectValue(property, 'key')) === '_tag'));
49
+ };
192
50
  const effectDefaultSpecs = [
193
51
  {
194
- name: 'effect-no-floating-effect',
195
- message: 'Return, yield, assign, or compose Effect values; bare Effect calls never execute.',
196
52
  check: hasFloatingEffect,
53
+ message: 'Return, yield, assign, or compose Effect values; bare Effect calls never execute.',
54
+ name: 'effect-no-floating-effect',
197
55
  },
198
56
  {
199
- name: 'effect-require-yield-star',
57
+ check: hasYieldWithoutStarInGen,
200
58
  message: 'Use yield* inside Effect.gen so generator composition unwraps the Effect value.',
59
+ name: 'effect-require-yield-star',
201
60
  tokenGroups: [['gen'], ['yield']],
202
- check: hasYieldWithoutStarInGen,
203
61
  },
204
62
  {
205
- name: 'effect-require-return-yield-star',
63
+ check: hasReturnEffectInGen,
206
64
  message: 'Do not return an Effect from Effect.gen; return a value or return yield* the Effect.',
65
+ name: 'effect-require-return-yield-star',
207
66
  tokenGroups: [['gen'], ['return']],
208
- check: hasReturnEffectInGen,
209
67
  },
210
68
  {
211
- name: 'effect-prefer-gen-for-nested-flatmap',
69
+ check: hasNestedFlatMap,
212
70
  message: 'Replace nested Effect.flatMap callbacks with Effect.gen for readable sequencing.',
71
+ name: 'effect-prefer-gen-for-nested-flatmap',
213
72
  tokens: ['flatMap'],
214
- check: hasNestedFlatMap,
215
73
  },
216
74
  {
217
- name: 'effect-no-function-returning-gen',
75
+ check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.gen\s*\(/.test(segment.trim())),
218
76
  message: 'Use Effect.fn for exported effectful functions instead of returning Effect.gen.',
77
+ name: 'effect-no-function-returning-gen',
219
78
  tokens: ['gen'],
220
- check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.gen\s*\(/.test(segment.trim())),
221
79
  },
222
80
  {
223
- name: 'effect-prefer-effect-fn-for-exported-effects',
81
+ check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.(?!fn\b|gen\b|isEffect\b|serviceFunction\b|runPromise\b)/.test(segment.trim())),
224
82
  message: 'Exported effectful functions should use Effect.fn for tracing and stable contracts.',
83
+ name: 'effect-prefer-effect-fn-for-exported-effects',
225
84
  tokens: ['export'],
226
- check: (source) => exportedCallableDeclarationSegments(source).some((segment) => /(?:^|\breturn\s+)Effect\.(?!fn\b|gen\b|isEffect\b|serviceFunction\b|runPromise\b)/.test(segment.trim())),
227
85
  },
228
86
  {
229
- name: 'effect-no-unnecessary-gen',
230
87
  message: 'Do not wrap a single Effect in Effect.gen when direct composition is clearer.',
231
- tokenGroups: [['gen'], ['yield']],
88
+ name: 'effect-no-unnecessary-gen',
232
89
  patterns: [/Effect\.gen\s*\(\s*function\*\s*\([^)]*\)\s*{\s*return\s+yield\*\s+Effect\./],
90
+ tokenGroups: [['gen'], ['yield']],
233
91
  },
234
92
  {
235
- name: 'effect-no-effect-in-array-foreach',
93
+ check: hasEffectInArrayForEach,
236
94
  message: 'Use Effect.forEach instead of Array.forEach with Effect-returning callbacks.',
95
+ name: 'effect-no-effect-in-array-foreach',
237
96
  tokens: ['forEach'],
238
- check: hasEffectInArrayForEach,
239
97
  },
240
98
  {
241
- name: 'effect-no-effect-in-promise-callback',
99
+ check: hasEffectInPromiseCallback,
242
100
  message: 'Do not create Effect values inside Promise callbacks; compose at the Effect boundary.',
101
+ name: 'effect-no-effect-in-promise-callback',
243
102
  tokens: ['.then', '.catch'],
244
- check: hasEffectInPromiseCallback,
245
103
  },
246
104
  {
247
- name: 'effect-no-floating-fiber',
105
+ check: hasUnobservedFork,
248
106
  message: 'Forked fibers must be joined, interrupted, scoped, supervised, or returned.',
107
+ name: 'effect-no-floating-fiber',
249
108
  tokens: ['fork'],
250
- check: hasUnobservedFork,
251
109
  },
252
110
  {
253
- name: 'effect-require-suspend-for-recursion',
111
+ check: hasRecursiveEffectWithoutSuspend,
254
112
  message: 'Recursive Effect construction must be wrapped in Effect.suspend.',
113
+ name: 'effect-require-suspend-for-recursion',
255
114
  tokens: ['function', '=>'],
256
- check: hasRecursiveEffectWithoutSuspend,
257
115
  },
258
116
  {
259
- name: 'effect-require-suspend-for-lazy-evaluation',
117
+ check: hasUnsafeLazyEvaluation,
260
118
  message: 'Use Effect.suspend when Effect construction must defer eager JavaScript work.',
119
+ name: 'effect-require-suspend-for-lazy-evaluation',
261
120
  tokens: ['Date.now', 'Math.random', 'new Date', 'JSON.parse'],
262
- check: hasUnsafeLazyEvaluation,
263
121
  },
264
122
  {
265
- name: 'effect-no-async-await-in-effect',
123
+ check: hasAsyncAwaitInEffect,
266
124
  message: 'Use Effect.tryPromise or Effect.promise boundaries instead of async/await in Effect code.',
125
+ name: 'effect-no-async-await-in-effect',
267
126
  tokens: ['async', 'await'],
268
- check: hasAsyncAwaitInEffect,
269
127
  },
270
128
  {
271
- name: 'effect-no-promise-then-in-effect',
272
- message: 'Use Effect combinators instead of Promise then/catch chains in Effect modules.',
273
- tokens: ['.then', '.catch'],
274
129
  ast: (context, source) => {
275
130
  const isEffectModule = hasEffectSignal(source);
276
131
  return {
@@ -278,601 +133,171 @@ const effectDefaultSpecs = [
278
133
  if (!isEffectModule) {
279
134
  return;
280
135
  }
281
- const { callee } = node;
282
- const { propertyName } = memberParts(callee);
136
+ const { propertyName } = memberParts(objectValue(node, 'callee'));
283
137
  if (propertyName === 'then' || propertyName === 'catch') {
284
- reportAst(context, 'Use Effect combinators instead of Promise then/catch chains in Effect modules.', node);
138
+ reportAST(context, 'Use Effect combinators instead of Promise then/catch chains in Effect modules.', node);
285
139
  }
286
140
  },
287
141
  };
288
142
  },
143
+ message: 'Use Effect combinators instead of Promise then/catch chains in Effect modules.',
144
+ name: 'effect-no-promise-then-in-effect',
145
+ tokens: ['.then', '.catch'],
289
146
  },
290
147
  {
291
- name: 'effect-no-throw',
148
+ check: hasThrowInEffect,
292
149
  message: 'Use typed Effect failures instead of throw inside Effect workflows.',
150
+ name: 'effect-no-throw',
293
151
  tokens: ['throw'],
294
- check: hasThrowInEffect,
295
152
  },
296
153
  {
297
- name: 'effect-no-string-errors',
298
- message: 'Use structured tagged errors instead of string failures.',
299
- tokens: ['fail'],
300
- check: hasStringErrorFailure,
301
154
  ast: (context, source) => {
302
155
  const isEffectFail = effectCallPredicate(source, ['fail']);
303
156
  return {
304
157
  CallExpression(node) {
305
- const call = node;
306
- if (isEffectFail(call.callee) && isStringLikeLiteral(call.arguments?.[0])) {
307
- reportAst(context, 'Use structured tagged errors instead of string failures.', node);
158
+ const callArguments = arrayValue(objectValue(node, 'arguments'));
159
+ if (isEffectFail(objectValue(node, 'callee')) && isStringLikeLiteral(callArguments[0])) {
160
+ reportAST(context, 'Use structured tagged errors instead of string failures.', node);
308
161
  }
309
162
  },
310
163
  };
311
164
  },
165
+ check: hasStringErrorFailure,
166
+ message: 'Use structured tagged errors instead of string failures.',
167
+ name: 'effect-no-string-errors',
168
+ tokens: ['fail'],
312
169
  },
313
170
  {
314
- name: 'effect-no-untagged-errors',
315
- message: 'Use tagged/data/schema errors in Effect.fail instead of global Error values.',
316
- tokens: ['fail'],
317
- check: hasUntaggedErrorFailure,
318
171
  ast: (context, source) => {
319
172
  const isEffectFail = effectCallPredicate(source, ['fail']);
320
173
  return {
321
174
  CallExpression(node) {
322
- const call = node;
323
- const firstArg = call.arguments?.[0];
324
- if (!isEffectFail(call.callee)) {
175
+ const callArguments = arrayValue(objectValue(node, 'arguments'));
176
+ const [firstArg] = callArguments;
177
+ if (!isEffectFail(objectValue(node, 'callee'))) {
325
178
  return;
326
179
  }
327
180
  if (!firstArg) {
328
181
  return;
329
182
  }
330
- if (nodeType(firstArg) === 'NewExpression' &&
331
- identifierName(firstArg.callee) === 'Error') {
332
- reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
333
- return;
334
- }
335
- if (nodeType(firstArg) === 'ObjectExpression' &&
336
- !firstArg.properties?.some((property) => propertyKeyName(property.key) === '_tag')) {
337
- reportAst(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
183
+ if (isUntaggedErrorFailureArgument(firstArg)) {
184
+ reportAST(context, 'Use tagged/data/schema errors in Effect.fail instead of global Error values.', node);
338
185
  }
339
186
  },
340
187
  };
341
188
  },
189
+ check: hasUntaggedErrorFailure,
190
+ message: 'Use tagged/data/schema errors in Effect.fail instead of global Error values.',
191
+ name: 'effect-no-untagged-errors',
192
+ tokens: ['fail'],
342
193
  },
343
194
  {
344
- name: 'effect-no-silent-error-swallowing',
345
195
  message: 'Do not erase Effect failures without recovery, logging, or explicit typed replacement.',
346
- tokens: ['catchAll', 'ignore'],
196
+ name: 'effect-no-silent-error-swallowing',
347
197
  patterns: [
348
198
  /Effect\.(?:catchAll|ignore)\s*\([\s\S]*?(?:Effect\.void|Effect\.succeed\s*\(\s*undefined|undefined)/,
349
199
  ],
200
+ tokens: ['catchAll', 'ignore'],
350
201
  },
351
202
  {
352
- name: 'effect-require-typed-error-in-trypromise',
203
+ check: hasTryPromiseWithoutTypedCatch,
353
204
  message: 'Use Effect.tryPromise({ try, catch }) so Promise failures become typed Effect errors.',
205
+ name: 'effect-require-typed-error-in-trypromise',
354
206
  tokens: ['tryPromise'],
355
- check: hasTryPromiseWithoutTypedCatch,
356
207
  },
357
208
  {
358
- name: 'effect-prefer-catchTag-over-catchAll',
209
+ check: hasBroadCatchAllWithoutRethrow,
359
210
  message: 'Prefer catchTag or catchTags over broad catchAll recovery.',
211
+ name: 'effect-prefer-catchTag-over-catchAll',
360
212
  tokens: ['catchAll'],
361
- check: hasBroadCatchAllWithoutRethrow,
362
213
  },
363
214
  {
364
- name: 'effect-no-catchAll-with-mapError',
365
215
  message: 'Use mapError directly instead of catchAll when only transforming the error.',
366
- tokens: ['catchAll'],
216
+ name: 'effect-no-catchAll-with-mapError',
367
217
  patterns: [/Effect\.catchAll\s*\([\s\S]*?=>\s*Effect\.fail\s*\(/],
218
+ tokens: ['catchAll'],
368
219
  },
369
220
  {
370
- name: 'effect-prefer-mapError-over-catchAll-rethrow',
371
221
  message: 'Use Effect.mapError instead of catchAll followed by fail.',
372
- tokens: ['catchAll'],
222
+ name: 'effect-prefer-mapError-over-catchAll-rethrow',
373
223
  patterns: [/(?:^|[^\w$.])catchAll\s*\([\s\S]*?=>\s*Effect\.fail\s*\(/],
224
+ tokens: ['catchAll'],
374
225
  },
375
226
  {
376
- name: 'effect-require-error-cause-preserved',
227
+ check: hasErrorMappingWithoutCause,
377
228
  message: 'Preserve the original cause when mapping or wrapping Effect errors.',
229
+ name: 'effect-require-error-cause-preserved',
378
230
  tokens: ['mapError', 'catchAll'],
379
- check: hasErrorMappingWithoutCause,
380
231
  },
381
232
  {
382
- name: 'effect-prefer-ignore-logged',
233
+ check: hasUnloggedIgnore,
383
234
  message: 'Ignored Effect failures must be logged or otherwise observable.',
235
+ name: 'effect-prefer-ignore-logged',
384
236
  tokens: ['ignore'],
385
- check: hasUnloggedIgnore,
386
237
  },
387
238
  {
388
- name: 'effect-prefer-catchTags-for-multiple-tags',
239
+ check: hasMultipleCatchTagsInOnePipe,
389
240
  message: 'Use Effect.catchTags for multiple tagged Effect recoveries.',
241
+ name: 'effect-prefer-catchTags-for-multiple-tags',
390
242
  tokens: ['catchTag'],
391
- check: hasMultipleCatchTagsInOnePipe,
392
243
  },
393
244
  {
394
- name: 'effect-no-error-channel-widening-to-unknown',
395
245
  message: 'Do not widen Effect error channels to unknown.',
396
- tokens: ['unknown'],
246
+ name: 'effect-no-error-channel-widening-to-unknown',
397
247
  patterns: [/Effect\s*<[^>]*,\s*unknown\b/, /Effect\.fail\s*<\s*unknown\b/],
248
+ tokens: ['unknown'],
398
249
  },
399
250
  {
400
- name: 'effect-no-run-inside-effect',
251
+ check: hasRuntimeInEffect,
401
252
  message: 'Do not run an Effect from inside another Effect; yield or compose it instead.',
253
+ name: 'effect-no-run-inside-effect',
402
254
  tokens: ['run'],
403
- check: hasRuntimeInEffect,
404
255
  },
405
256
  {
406
- name: 'effect-no-runpromise-in-exported-api',
257
+ check: (source) => exportedDeclarationSegments(source).some((segment) => /Effect\.runPromise\s*\(/.test(segment)),
407
258
  message: 'Exported APIs should expose Effect values instead of hiding execution behind Promise.',
259
+ name: 'effect-no-runpromise-in-exported-api',
408
260
  tokens: ['runPromise'],
409
- check: (source) => exportedDeclarationSegments(source).some((segment) => /Effect\.runPromise\s*\(/.test(segment)),
410
261
  },
411
262
  {
412
- name: 'effect-no-runfork-without-observer',
263
+ check: hasRunForkWithoutObserver,
413
264
  message: 'Do not call runFork without explicit observation, supervision, or interruption.',
265
+ name: 'effect-no-runfork-without-observer',
414
266
  tokens: ['runFork'],
415
- check: hasRunForkWithoutObserver,
416
267
  },
417
268
  {
418
- name: 'effect-no-sync-for-promise',
269
+ check: hasSyncForPromise,
419
270
  message: 'Use Effect.tryPromise for Promise-returning code instead of Effect.sync.',
271
+ name: 'effect-no-sync-for-promise',
420
272
  tokenGroups: [['sync'], ['async', 'fetch', 'Promise.']],
421
- check: hasSyncForPromise,
422
273
  },
423
274
  {
424
- name: 'effect-no-sync-for-throwing-ops',
275
+ check: hasSyncForThrowingOPS,
425
276
  message: 'Use Effect.try for synchronous code that can throw instead of Effect.sync.',
277
+ name: 'effect-no-sync-for-throwing-ops',
426
278
  tokenGroups: [['sync'], ['throw', 'JSON.parse']],
427
- check: hasSyncForThrowingOps,
428
279
  },
429
280
  {
430
- name: 'effect-no-console-log-in-effect-code',
431
- message: 'Use Effect logging APIs instead of console logging in Effect code.',
432
- tokens: ['console'],
433
281
  ast: (context, source) => {
434
282
  const isEffectModule = hasEffectSignal(source);
435
283
  return {
436
284
  CallExpression(node) {
437
- const { objectName, propertyName } = memberParts(node.callee);
285
+ const { objectName, propertyName } = memberParts(objectValue(node, 'callee'));
438
286
  if (isEffectModule &&
439
287
  objectName === 'console' &&
440
288
  propertyName &&
441
289
  ['debug', 'error', 'info', 'log', 'warn'].includes(propertyName)) {
442
- reportAst(context, 'Use Effect logging APIs instead of console logging in Effect code.', node);
443
- }
444
- },
445
- };
446
- },
447
- },
448
- {
449
- name: 'effect-no-process-env-in-effect-code',
450
- message: 'Use Effect Config instead of process.env in Effect code.',
451
- tokens: ['process'],
452
- ast: (context, source) => {
453
- const isEffectModule = hasEffectSignal(source);
454
- const isConfigLayer = isConfiguredPath(context, 'configLayers');
455
- return {
456
- MemberExpression(node) {
457
- const { objectName, propertyName } = memberParts(node);
458
- const processEnv = objectName === 'process' && propertyName === 'env';
459
- if (isEffectModule && !isConfigLayer && processEnv) {
460
- reportAst(context, 'Use Effect Config instead of process.env in Effect code.', node);
461
- }
462
- },
463
- };
464
- },
465
- },
466
- {
467
- name: 'effect-no-date-now-in-effect-code',
468
- message: 'Use Effect Clock instead of Date.now in Effect code.',
469
- tokens: ['Date'],
470
- ast: (context, source) => {
471
- const isEffectModule = hasEffectSignal(source);
472
- const isAdapterLayer = isConfiguredPath(context, 'adapterLayers');
473
- return {
474
- CallExpression(node) {
475
- const { objectName, propertyName } = memberParts(node.callee);
476
- if (isEffectModule &&
477
- !isAdapterLayer &&
478
- objectName === 'Date' &&
479
- propertyName === 'now') {
480
- reportAst(context, 'Use Effect Clock instead of Date.now in Effect code.', node);
481
- }
482
- },
483
- };
484
- },
485
- },
486
- {
487
- name: 'effect-no-math-random-in-effect-code',
488
- message: 'Use Effect Random instead of Math.random in Effect code.',
489
- tokens: ['Math'],
490
- ast: (context, source) => {
491
- const isEffectModule = hasEffectSignal(source);
492
- const isAdapterLayer = isConfiguredPath(context, 'adapterLayers');
493
- return {
494
- CallExpression(node) {
495
- const { objectName, propertyName } = memberParts(node.callee);
496
- if (isEffectModule &&
497
- !isAdapterLayer &&
498
- objectName === 'Math' &&
499
- propertyName === 'random') {
500
- reportAst(context, 'Use Effect Random instead of Math.random in Effect code.', node);
501
- }
502
- },
503
- };
504
- },
505
- },
506
- {
507
- name: 'effect-no-json-parse-cast',
508
- message: 'Decode external JSON with Schema instead of casting parsed unknown data.',
509
- tokenGroups: [[' as '], ['JSON.parse', '.json']],
510
- patterns: [
511
- /\bJSON\.parse\s*\([^)]*\)\s+as\s+[A-Za-z_$][\w$]*/,
512
- /\.json\s*\(\s*\)\s+as\s+[A-Za-z_$][\w$]*/,
513
- ],
514
- },
515
- {
516
- name: 'effect-schema-prefer-decodeUnknown-effect',
517
- message: 'Use Schema.decodeUnknown to decode unknown input into the Effect error channel.',
518
- tokens: ['decode'],
519
- check: hasSchemaPromiseDecode,
520
- },
521
- {
522
- name: 'effect-schema-require-parse-error-handling',
523
- message: 'Schema parsing must expose parse errors through typed Effect handling.',
524
- tokens: ['decode'],
525
- check: hasUnhandledSchemaEffectDecode,
526
- },
527
- {
528
- name: 'effect-schema-use-decodeUnknown-for-external-data',
529
- message: 'External data must enter through Schema.decodeUnknown.',
530
- tokens: ['.json'],
531
- check: hasExternalJsonWithoutDecodeUnknown,
532
- },
533
- {
534
- name: 'effect-schema-no-unsafe-sync-decode-in-effect-code',
535
- message: 'Do not use throwing synchronous Schema decoders in Effect modules.',
536
- tokens: ['decodeSync', 'decodeUnknownSync'],
537
- check: hasSchemaSyncDecodeInEffectWorkflow,
538
- },
539
- {
540
- name: 'effect-schema-require-parseJson-for-json-strings',
541
- message: 'Use Schema.parseJson when decoding JSON strings with Schema.',
542
- tokens: ['JSON.parse'],
543
- check: hasJsonParsedBeforeSchemaStringDecode,
544
- },
545
- {
546
- name: 'effect-schema-correct-number-type-for-parsed-json',
547
- message: 'Use the correct Schema number type for already-parsed JSON numbers.',
548
- tokens: ['JSON.parse'],
549
- check: hasParsedJsonNumberFromString,
550
- },
551
- {
552
- name: 'effect-schema-prefer-taggedClass-over-manual-tag',
553
- message: 'Prefer Schema.TaggedClass over hand-written _tag fields.',
554
- tokens: ['_tag'],
555
- patterns: [/Schema\.Struct\s*\(\s*{[\s\S]*?_tag\s*:\s*Schema\.Literal/],
556
- },
557
- {
558
- name: 'effect-schema-avoid-old-type-names',
559
- message: 'Use current Effect Schema API names instead of obsolete lowercase helpers.',
560
- tokens: ['Schema.'],
561
- patterns: [/\bSchema\.(?:string|number|boolean|array|object)\s*\(/],
562
- },
563
- {
564
- name: 'effect-schema-no-cast-after-decode',
565
- message: 'Do not cast after Schema decoding; let the schema provide the type.',
566
- tokenGroups: [['Schema.decode'], [' as ']],
567
- check: hasCastAfterSchemaDecode,
568
- },
569
- {
570
- name: 'effect-require-acquire-release',
571
- message: 'Resource acquisition must use acquireRelease, scoped, or equivalent finalization.',
572
- tokens: ['open', 'connect', 'subscribe', 'listen'],
573
- check: hasUnreleasedAcquire,
574
- },
575
- {
576
- name: 'effect-require-scoped-for-acquireRelease',
577
- message: 'Use Effect.scoped around acquireRelease when exposing acquired resources.',
578
- tokens: ['acquireRelease'],
579
- check: hasUnscopedAcquireRelease,
580
- },
581
- {
582
- name: 'effect-require-scoped-for-resources',
583
- message: 'Resourceful workflows must be scoped.',
584
- tokens: ['Socket.', 'Connection.'],
585
- check: hasUnscopedResourceWorkflow,
586
- },
587
- {
588
- name: 'effect-no-fork-daemon-without-cleanup',
589
- message: 'Daemon fibers must have cleanup, interruption, or supervision.',
590
- tokens: ['forkDaemon'],
591
- check: hasForkDaemonWithoutCleanup,
592
- },
593
- {
594
- name: 'effect-prefer-fork-scoped-for-listeners',
595
- message: 'Long-running listeners should use forkScoped so they follow Scope lifetime.',
596
- tokens: ['fork'],
597
- patterns: [
598
- /Effect\.fork\s*\([\s\S]*?\b(?:listen[A-Z]\w*|subscribe[A-Z]\w*|watch[A-Z]\w*|listen|subscribe|watch)\b/,
599
- ],
600
- },
601
- {
602
- name: 'effect-require-restore-for-fork-in-uninterruptible',
603
- message: 'Use restore when forking inside uninterruptible regions.',
604
- tokenGroups: [['uninterruptible'], ['fork']],
605
- check: hasForkInUninterruptibleWithoutRestore,
606
- },
607
- {
608
- name: 'effect-require-bounded-concurrency',
609
- message: 'Concurrent Effect traversal must declare an explicit concurrency bound.',
610
- tokens: ['concurrency'],
611
- check: hasUnboundedEffectConcurrency,
612
- },
613
- {
614
- name: 'effect-require-bounded-flatMap-concurrency',
615
- message: 'Concurrent Effect.flatMap usage must declare a bounded concurrency value.',
616
- tokenGroups: [['flatMap'], ['concurrency']],
617
- check: hasUnboundedFlatMapConcurrency,
618
- },
619
- {
620
- name: 'effect-no-unbounded-queue',
621
- message: 'Use bounded, sliding, or dropping queues instead of unbounded queues.',
622
- tokens: ['unbounded'],
623
- patterns: [/\bQueue\.unbounded\s*\(/],
624
- },
625
- {
626
- name: 'effect-no-unbounded-stream-buffer',
627
- message: 'Stream buffers must be explicitly bounded.',
628
- tokens: ['unbounded', 'Infinity'],
629
- patterns: [
630
- /\bStream\.(?:buffer|fromQueue|async|asyncPush)\s*\([^)]*\b(?:Infinity|unbounded)\b/,
631
- ],
632
- },
633
- {
634
- name: 'effect-test-no-runpromise',
635
- message: 'Use @effect/vitest it.effect instead of manually running Effects in tests.',
636
- tokens: ['run'],
637
- check: (source, context) => isEffectTestPath(context) && hasRuntimeCall(source),
638
- },
639
- {
640
- name: 'effect-prefer-it-effect-for-unit-tests',
641
- message: 'Use it.effect for tests that exercise Effect programs.',
642
- tokens: ['it('],
643
- check: (source, context) => isEffectTestPath(context) &&
644
- !hasRuntimeCall(source) &&
645
- /\bit\s*\([\s\S]*?Effect\./.test(stripCommentsAndStrings(source)),
646
- },
647
- {
648
- name: 'effect-testClock-requires-fork',
649
- message: 'Fork time-dependent work before adjusting TestClock.',
650
- tokens: ['TestClock.adjust'],
651
- check: (source) => /TestClock\.adjust\s*\(/.test(stripCommentsAndStrings(source)) &&
652
- !hasForkBeforeTestClockAdjust(source),
653
- },
654
- {
655
- name: 'effect-testClock-requires-testContext',
656
- message: 'Use Effect test context when using TestClock.',
657
- tokens: ['TestClock'],
658
- check: (source, context) => isEffectTestPath(context) && hasTestClockWithoutEffectContext(source),
659
- },
660
- {
661
- name: 'effect-no-real-sleep-in-tests',
662
- message: 'Use TestClock instead of real sleeps in Effect tests.',
663
- tokens: ['sleep'],
664
- check: (source, context) => isEffectTestPath(context) && hasRealSleepWithoutTestClock(source),
665
- },
666
- {
667
- name: 'effect-use-exit-for-failure-tests',
668
- message: 'Use Effect.exit inside it.effect when asserting Effect failures.',
669
- tokens: ['rejects', 'toThrow'],
670
- check: (source, context) => isEffectTestPath(context) &&
671
- !hasRuntimeCall(source) &&
672
- /\b(?:rejects|toThrow)\b/.test(stripCommentsAndStrings(source)) &&
673
- /\bEffect\./.test(stripCommentsAndStrings(source)),
674
- },
675
- {
676
- name: 'effect-no-focused-effect-tests',
677
- message: 'Focused Effect tests must not be committed.',
678
- tokens: ['.only'],
679
- check: (source, context) => isEffectTestPath(context) &&
680
- /\b(?:it|describe)\.effect\.only\s*\(/.test(stripCommentsAndStrings(source)),
681
- },
682
- {
683
- name: 'effect-no-skipped-effect-tests',
684
- message: 'Skipped Effect tests must not be committed.',
685
- tokens: ['.skip'],
686
- check: (source, context) => isEffectTestPath(context) &&
687
- /\b(?:it|describe)\.effect\.skip\s*\(/.test(stripCommentsAndStrings(source)),
688
- },
689
- {
690
- name: 'effect-no-obsolete-imports',
691
- message: 'Import Effect APIs from the main effect package; deprecated split packages are blocked.',
692
- tokens: ['@effect/io', '@effect/data'],
693
- patterns: [/from\s+['"]@effect\/(?:io|data)['"]/],
694
- },
695
- {
696
- name: 'effect-no-known-fake-api',
697
- message: 'This is not a known Effect API for the configured version.',
698
- tokens: ['fromPromise', 'tryCatch', 'bracket', 'fromEither'],
699
- check: (source) => /\bEffect\.(?:fromPromise|tryCatch|bracket|fromEither)\s*\(/.test(stripCommentsAndStrings(source)),
700
- },
701
- {
702
- name: 'effect-prefer-gen-over-do',
703
- message: 'Prefer Effect.gen over Effect.Do for agent-readable sequential workflows.',
704
- tokens: ['Effect.Do'],
705
- patterns: [/\bEffect\.Do\b/],
706
- },
707
- {
708
- name: 'effect-prefer-direct-yield-star',
709
- message: 'Use direct yield* effect style instead of adapter-style Effect.gen helpers.',
710
- tokenGroups: [['gen'], ['$']],
711
- patterns: [/Effect\.gen\s*\(\s*function\*\s*\(\s*\$\s*\)/],
712
- },
713
- {
714
- name: 'effect-prefer-config-redacted',
715
- message: 'Use Config.redacted for sensitive config values so secrets stay redacted in logs and errors.',
716
- tokens: ['secret', 'Secret'],
717
- patterns: [/Config\.(?:secret|Secret)\s*\(/],
718
- },
719
- {
720
- name: 'effect-no-deprecated-schema-package',
721
- message: 'Use Schema from effect/Schema instead of @effect/schema.',
722
- tokens: ['@effect/schema'],
723
- patterns: [/from\s+['"]@effect\/schema['"]/],
724
- },
725
- {
726
- name: 'effect-no-deprecated-context-tag-function',
727
- message: 'Use the current Context.Tag class/service pattern instead of deprecated tag helpers.',
728
- tokens: ['Context.Tag'],
729
- patterns: [
730
- /\b(?:const|let|var)\s+[A-Z][\w$]*\s*=\s*Context\.Tag(?:<[^>]+>)?\s*\(\s*['"][^'"]+['"]\s*\)/,
731
- /(?:^|[;\n]\s*)Context\.Tag(?:<[^>]+>)?\s*\(\s*['"][^'"]+['"]\s*\)/,
732
- ],
733
- },
734
- {
735
- name: 'effect-no-global-error-channel',
736
- message: 'Use domain-specific tagged errors instead of the global Error type channel.',
737
- tokenGroups: [['Effect.Effect'], ['Error']],
738
- patterns: [/Effect\.Effect\s*<[^,>]+,\s*Error\s*[,>]/],
739
- },
740
- {
741
- name: 'effect-use-duration-constructors',
742
- message: 'Use Duration constructors or string durations instead of naked millisecond numbers.',
743
- tokens: ['sleep', 'timeout', 'delay'],
744
- patterns: [
745
- /Effect\.(?:sleep|timeout|delay)\s*\(\s*\d+\s*\)/,
746
- /Effect\.(?:timeout|delay)\s*\([^,]+,\s*\d+\s*\)/,
747
- ],
748
- },
749
- {
750
- name: 'effect-no-mixed-effect-import-styles',
751
- message: 'Use one Effect import style per file.',
752
- tokens: ['import'],
753
- check: hasMixedEffectImportStyles,
754
- },
755
- {
756
- name: 'effect-prefer-effect-is',
757
- message: 'Use Effect.isEffect for Effect type checks.',
758
- tokens: ['instanceof', '_op'],
759
- patterns: [/\b[A-Za-z_$][\w$]*\s+instanceof\s+Effect\b/, /\._op\s*===\s*['"]Effect['"]/],
760
- },
761
- {
762
- name: 'effect-no-try-catch-in-effect-gen',
763
- message: 'Use Effect error combinators instead of try/catch inside Effect.gen.',
764
- tokenGroups: [['gen'], ['try']],
765
- ast: (context, source) => ({
766
- TryStatement(node) {
767
- const { start } = node;
768
- if (typeof start !== 'number') {
769
- return;
770
- }
771
- const index = hasTryCatchInEffectGen(source);
772
- if (typeof index === 'number' && index === start) {
773
- reportAst(context, 'Use Effect error combinators instead of try/catch inside Effect.gen.', node);
774
- }
775
- },
776
- }),
777
- },
778
- {
779
- name: 'effect-no-new-promise',
780
- message: 'Use Effect.async, Effect.promise, or Effect.tryPromise instead of new Promise.',
781
- tokenGroups: [['Promise'], ['Effect', 'effect']],
782
- ast: (context, source) => {
783
- const isEffectModule = hasEffectSignal(source);
784
- return {
785
- NewExpression(node) {
786
- if (isEffectModule && identifierName(node.callee) === 'Promise') {
787
- reportAst(context, 'Use Effect.async, Effect.promise, or Effect.tryPromise instead of new Promise.', node);
788
- }
789
- },
790
- };
791
- },
792
- },
793
- {
794
- name: 'effect-no-global-timers',
795
- message: 'Use Effect.sleep, Schedule, or Clock instead of global timers in Effect modules.',
796
- tokens: ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'],
797
- ast: (context, source) => {
798
- const isEffectModule = hasEffectSignal(source);
799
- return {
800
- CallExpression(node) {
801
- const calleeName = identifierName(node.callee);
802
- if (isEffectModule &&
803
- calleeName &&
804
- ['clearInterval', 'clearTimeout', 'setInterval', 'setTimeout'].includes(calleeName)) {
805
- reportAst(context, 'Use Effect.sleep, Schedule, or Clock instead of global timers in Effect modules.', node);
806
- }
807
- },
808
- };
809
- },
810
- },
811
- {
812
- name: 'effect-no-native-error-classes',
813
- message: 'Use tagged/data/schema errors instead of classes extending native Error.',
814
- tokens: ['Error'],
815
- check: hasNativeErrorClassInEffectModule,
816
- ast: (context, source) => {
817
- const isEffectModule = hasEffectSignal(source);
818
- return {
819
- ClassDeclaration(node) {
820
- if (isEffectModule && identifierName(node.superClass) === 'Error') {
821
- reportAst(context, 'Use tagged/data/schema errors instead of classes extending native Error.', node);
822
- }
823
- },
824
- };
825
- },
826
- },
827
- {
828
- name: 'effect-no-unsafe-effect-type-assertion',
829
- message: 'Do not assert Effect error or requirement channels with type casts.',
830
- tokenGroups: [[' as '], ['Effect.Effect']],
831
- check: hasUnsafeEffectTypeAssertion,
832
- ast: (context) => ({
833
- TSAsExpression(node) {
834
- if (typeReferenceName(node.typeAnnotation) === 'Effect.Effect') {
835
- reportAst(context, 'Do not assert Effect error or requirement channels with type casts.', node);
836
- }
837
- },
838
- }),
839
- },
840
- {
841
- name: 'effect-require-service-self-match',
842
- message: 'Effect service/tag self type must match the declaring class name.',
843
- tokenGroups: [['class'], ['extends'], ['Context', 'Service', 'Tag']],
844
- check: hasServiceSelfMismatch,
845
- ast: (context, source) => ({
846
- ClassDeclaration(node) {
847
- const className = identifierName(node.id);
848
- const selfName = effectServiceSelfName(node.superClass, source);
849
- if (className && selfName && className !== selfName) {
850
- reportAst(context, 'Effect service/tag self type must match the declaring class name.', node);
851
- }
852
- },
853
- }),
854
- },
855
- {
856
- name: 'effect-no-effect-fn-iife',
857
- message: 'Do not call Effect.fn as an IIFE; use Effect.gen for local one-shot workflows.',
858
- tokenGroups: [['fn'], ['Effect', 'effect']],
859
- check: hasEffectFnIife,
860
- ast: (context, source) => {
861
- const isEffectFn = effectCallPredicate(source, ['fn', 'fnUntraced', 'fnUntracedEager']);
862
- return {
863
- CallExpression(node) {
864
- const outerCallee = node.callee;
865
- const middleCallee = outerCallee?.callee;
866
- const innerCallee = middleCallee?.callee;
867
- if (nodeType(outerCallee) === 'CallExpression' &&
868
- nodeType(middleCallee) === 'CallExpression' &&
869
- isEffectFn(innerCallee)) {
870
- reportAst(context, 'Do not call Effect.fn as an IIFE; use Effect.gen for local one-shot workflows.', node);
290
+ reportAST(context, 'Use Effect logging APIs instead of console logging in Effect code.', node);
871
291
  }
872
292
  },
873
293
  };
874
294
  },
295
+ message: 'Use Effect logging APIs instead of console logging in Effect code.',
296
+ name: 'effect-no-console-log-in-effect-code',
297
+ tokens: ['console'],
875
298
  },
299
+ ...effectDefaultEnvironmentSpecs,
300
+ ...effectDefaultCompatibilitySpecs,
876
301
  ];
877
302
  const effectDefaultRules = makeRules(effectDefaultSpecs, {
878
303
  defaultTokens: effectDefaultRuleTokens,