@effect/language-service 0.81.0 → 0.82.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.
package/README.md CHANGED
@@ -10,7 +10,9 @@ This package implements a TypeScript language service plugin that allows additio
10
10
  2. Inside your tsconfig.json, you should add the plugin configuration as follows:
11
11
  ```jsonc
12
12
  {
13
- "$schema": "https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json",
13
+ "$schema": "./node_modules/@effect/language-service/schema.json",
14
+ // or from Github (may drift from your local installation)
15
+ // "$schema": ""https://raw.githubusercontent.com/Effect-TS/language-service/refs/heads/main/schema.json",
14
16
  "compilerOptions": {
15
17
  "plugins": [
16
18
  // ... other LSPs (if any) and as last
@@ -54,6 +56,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu
54
56
  <tr><td><code>anyUnknownInErrorContext</code></td><td>➖</td><td></td><td>Detects 'any' or 'unknown' types in Effect error or requirements channels</td><td>✓</td><td>✓</td></tr>
55
57
  <tr><td><code>classSelfMismatch</code></td><td>❌</td><td>🔧</td><td>Ensures Self type parameter matches the class name in Service/Tag/Schema classes</td><td>✓</td><td>✓</td></tr>
56
58
  <tr><td><code>duplicatePackage</code></td><td>⚠️</td><td></td><td>Detects when multiple versions of the same Effect package are loaded</td><td>✓</td><td>✓</td></tr>
59
+ <tr><td><code>effectFnImplicitAny</code></td><td>❌</td><td></td><td>Mirrors noImplicitAny for unannotated Effect.fn and Effect.fnUntraced callback parameters when no outer contextual function type exists</td><td>✓</td><td>✓</td></tr>
57
60
  <tr><td><code>floatingEffect</code></td><td>❌</td><td></td><td>Ensures Effects are yielded or assigned to variables, not left floating</td><td>✓</td><td>✓</td></tr>
58
61
  <tr><td><code>genericEffectServices</code></td><td>⚠️</td><td></td><td>Prevents services with type parameters that cannot be discriminated at runtime</td><td>✓</td><td>✓</td></tr>
59
62
  <tr><td><code>missingEffectContext</code></td><td>❌</td><td></td><td>Reports missing service requirements in Effect context channel</td><td>✓</td><td>✓</td></tr>
package/cli.js CHANGED
@@ -25719,7 +25719,7 @@ var runWith2 = (command, config) => {
25719
25719
  // package.json
25720
25720
  var package_default = {
25721
25721
  name: "@effect/language-service",
25722
- version: "0.81.0",
25722
+ version: "0.82.0",
25723
25723
  publishConfig: {
25724
25724
  access: "public",
25725
25725
  directory: "dist"
@@ -32076,6 +32076,7 @@ var classSelfMismatch = createDiagnostic({
32076
32076
  if (ts.isClassDeclaration(node) && node.name && node.heritageClauses) {
32077
32077
  const result3 = yield* pipe(
32078
32078
  typeParser.extendsEffectService(node),
32079
+ orElse5(() => typeParser.extendsServiceMapService(node)),
32079
32080
  orElse5(() => typeParser.extendsContextTag(node)),
32080
32081
  orElse5(() => typeParser.extendsEffectTag(node)),
32081
32082
  orElse5(() => typeParser.extendsSchemaClass(node)),
@@ -32451,6 +32452,89 @@ var effectFnIife = createDiagnostic({
32451
32452
  })
32452
32453
  });
32453
32454
 
32455
+ // src/diagnostics/effectFnImplicitAny.ts
32456
+ var getParameterName = (typescript, name) => {
32457
+ if (typescript.isIdentifier(name)) {
32458
+ return typescript.idText(name);
32459
+ }
32460
+ return "parameter";
32461
+ };
32462
+ var hasOuterContextualFunctionType = (typescript, typeChecker, node) => {
32463
+ const contextualType = typeChecker.getContextualType(node);
32464
+ if (!contextualType) {
32465
+ return false;
32466
+ }
32467
+ return typeChecker.getSignaturesOfType(contextualType, typescript.SignatureKind.Call).length > 0;
32468
+ };
32469
+ var effectFnImplicitAny = createDiagnostic({
32470
+ name: "effectFnImplicitAny",
32471
+ code: 54,
32472
+ description: "Mirrors noImplicitAny for unannotated Effect.fn and Effect.fnUntraced callback parameters when no outer contextual function type exists",
32473
+ group: "correctness",
32474
+ severity: "error",
32475
+ fixable: false,
32476
+ supportedEffect: ["v3", "v4"],
32477
+ apply: fn3("effectFnImplicitAny.apply")(function* (sourceFile, report) {
32478
+ const ts = yield* service2(TypeScriptApi);
32479
+ const program = yield* service2(TypeScriptProgram);
32480
+ const typeChecker = yield* service2(TypeCheckerApi);
32481
+ const typeParser = yield* service2(TypeParser);
32482
+ const noImplicitAny = program.getCompilerOptions().noImplicitAny ?? program.getCompilerOptions().strict ?? false;
32483
+ if (!noImplicitAny) {
32484
+ return;
32485
+ }
32486
+ const nodeToVisit = [sourceFile];
32487
+ const appendNodeToVisit = (node) => {
32488
+ nodeToVisit.push(node);
32489
+ return void 0;
32490
+ };
32491
+ while (nodeToVisit.length > 0) {
32492
+ const node = nodeToVisit.pop();
32493
+ ts.forEachChild(node, appendNodeToVisit);
32494
+ const parsed = yield* pipe(
32495
+ typeParser.effectFn(node),
32496
+ map12((result3) => ({
32497
+ call: result3.node,
32498
+ fn: result3.regularFunction
32499
+ })),
32500
+ orElse5(
32501
+ () => pipe(
32502
+ typeParser.effectFnGen(node),
32503
+ map12((result3) => ({
32504
+ call: result3.node,
32505
+ fn: result3.generatorFunction
32506
+ }))
32507
+ )
32508
+ ),
32509
+ orElse5(
32510
+ () => pipe(
32511
+ typeParser.effectFnUntracedGen(node),
32512
+ map12((result3) => ({
32513
+ call: result3.node,
32514
+ fn: result3.generatorFunction
32515
+ }))
32516
+ )
32517
+ ),
32518
+ orUndefined
32519
+ );
32520
+ if (!parsed || hasOuterContextualFunctionType(ts, typeChecker, parsed.call)) {
32521
+ continue;
32522
+ }
32523
+ for (const parameter of parsed.fn.parameters) {
32524
+ if (parameter.type || parameter.initializer) {
32525
+ continue;
32526
+ }
32527
+ const parameterName = getParameterName(ts, parameter.name);
32528
+ report({
32529
+ location: parameter.name,
32530
+ messageText: `Parameter '${parameterName}' implicitly has an 'any' type in Effect.fn/Effect.fnUntraced. Add an explicit type annotation or provide a contextual function type.`,
32531
+ fixes: []
32532
+ });
32533
+ }
32534
+ }
32535
+ })
32536
+ });
32537
+
32454
32538
  // src/diagnostics/effectFnOpportunity.ts
32455
32539
  var effectFnOpportunity = createDiagnostic({
32456
32540
  name: "effectFnOpportunity",
@@ -33985,6 +34069,24 @@ var leakingRequirements = createDiagnostic({
33985
34069
  const typeCheckerUtils = yield* service2(TypeCheckerUtils);
33986
34070
  const typeParser = yield* service2(TypeParser);
33987
34071
  const tsUtils = yield* service2(TypeScriptUtils);
34072
+ const isExpectedLeakingServiceSuppressed = (leakedServiceName, startNode) => {
34073
+ const sourceFile2 = tsUtils.getSourceFileOfNode(startNode);
34074
+ if (!sourceFile2) return false;
34075
+ return !!ts.findAncestor(startNode, (current) => {
34076
+ const ranges = ts.getLeadingCommentRanges(sourceFile2.text, current.pos) ?? [];
34077
+ const isSuppressed = ranges.some((range2) => {
34078
+ const commentText = sourceFile2.text.slice(range2.pos, range2.end);
34079
+ return commentText.split("\n").filter((line) => line.includes("@effect-expect-leaking")).some((line) => {
34080
+ const markerIndex = line.indexOf("@effect-expect-leaking");
34081
+ return markerIndex !== -1 && line.slice(markerIndex + "@effect-expect-leaking".length).includes(leakedServiceName);
34082
+ });
34083
+ });
34084
+ if (isSuppressed) {
34085
+ return true;
34086
+ }
34087
+ return ts.isClassDeclaration(current) || ts.isVariableStatement(current) || ts.isExpressionStatement(current) || ts.isStatement(current) ? "quit" : false;
34088
+ });
34089
+ };
33988
34090
  const parseLeakedRequirements = cachedBy(
33989
34091
  fn3("leakingServices.checkServiceLeaking")(
33990
34092
  function* (service3, atLocation) {
@@ -34066,8 +34168,12 @@ var leakingRequirements = createDiagnostic({
34066
34168
  (_, service3) => service3
34067
34169
  );
34068
34170
  function reportLeakingRequirements(node, requirements) {
34069
- if (requirements.length === 0) return;
34070
- const requirementsStr = requirements.map((_) => typeChecker.typeToString(_)).join(" | ");
34171
+ const filteredRequirements = requirements.filter(
34172
+ (requirement) => !isExpectedLeakingServiceSuppressed(typeChecker.typeToString(requirement), node)
34173
+ );
34174
+ if (filteredRequirements.length === 0) return;
34175
+ const requirementsStr = filteredRequirements.map((_) => typeChecker.typeToString(_)).join(" | ");
34176
+ const firstStr = typeChecker.typeToString(filteredRequirements[0]);
34071
34177
  report({
34072
34178
  location: node,
34073
34179
  messageText: `Methods of this Service require \`${requirementsStr}\` from every caller.
@@ -34076,7 +34182,7 @@ This leaks implementation details into the service's public type \u2014 callers
34076
34182
 
34077
34183
  Resolve these dependencies at Layer creation and provide them to each method, so the service's type reflects its purpose, not its implementation.
34078
34184
 
34079
- To suppress this diagnostic for specific dependency types that are intentionally passed through (e.g., HttpServerRequest), add \`@effect-leakable-service\` JSDoc to their interface declarations (e.g., the \`${typeChecker.typeToString(requirements[0])}\` interface), not to this service.
34185
+ To suppress this diagnostic for specific dependency types that are intentionally passed through (e.g., HttpServerRequest), add \`@effect-leakable-service\` JSDoc to their interface declarations (e.g., the \`${firstStr}\` interface), or to this service by adding a \`@effect-expect-leaking ${firstStr}\` JSDoc.
34080
34186
 
34081
34187
  More info and examples at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage`,
34082
34188
  fixes: []
@@ -37328,6 +37434,7 @@ var diagnostics = [
37328
37434
  catchUnfailableEffect,
37329
37435
  classSelfMismatch,
37330
37436
  duplicatePackage,
37437
+ effectFnImplicitAny,
37331
37438
  effectGenUsesAdapter,
37332
37439
  missingEffectContext,
37333
37440
  missingEffectError,
@@ -37411,6 +37518,8 @@ var formatDiagnosticForJson = (diagnostic, tsInstance) => {
37411
37518
  const diagnosticName = Object.values(diagnostics).find((_) => _.code === diagnostic.code)?.name ?? `effect(${diagnostic.code})`;
37412
37519
  return {
37413
37520
  file: diagnostic.file.fileName,
37521
+ start: diagnostic.start,
37522
+ length: diagnostic.length ?? 0,
37414
37523
  line: line + 1,
37415
37524
  column: character + 1,
37416
37525
  endLine: endLine + 1,
@@ -40722,6 +40831,27 @@ var metadata_default = {
40722
40831
  diagnostics: []
40723
40832
  }
40724
40833
  },
40834
+ {
40835
+ name: "effectFnImplicitAny",
40836
+ group: "correctness",
40837
+ description: "Mirrors noImplicitAny for unannotated Effect.fn and Effect.fnUntraced callback parameters when no outer contextual function type exists",
40838
+ defaultSeverity: "error",
40839
+ fixable: false,
40840
+ supportedEffect: [
40841
+ "v3",
40842
+ "v4"
40843
+ ],
40844
+ preview: {
40845
+ sourceText: 'import * as Effect from "effect/Effect"\n\nexport const preview = Effect.fn("preview")((input) => Effect.succeed(input))\n',
40846
+ diagnostics: [
40847
+ {
40848
+ start: 86,
40849
+ end: 91,
40850
+ text: "Parameter 'input' implicitly has an 'any' type in Effect.fn/Effect.fnUntraced. Add an explicit type annotation or provide a contextual function type. effect(effectFnImplicitAny)"
40851
+ }
40852
+ ]
40853
+ }
40854
+ },
40725
40855
  {
40726
40856
  name: "floatingEffect",
40727
40857
  group: "correctness",
@@ -41171,7 +41301,7 @@ var metadata_default = {
41171
41301
  {
41172
41302
  start: 212,
41173
41303
  end: 217,
41174
- text: "Methods of this Service require `FileSystem` from every caller.\n\nThis leaks implementation details into the service's public type \u2014 callers shouldn't need to know *how* the service works internally, only *what* it provides.\n\nResolve these dependencies at Layer creation and provide them to each method, so the service's type reflects its purpose, not its implementation.\n\nTo suppress this diagnostic for specific dependency types that are intentionally passed through (e.g., HttpServerRequest), add `@effect-leakable-service` JSDoc to their interface declarations (e.g., the `FileSystem` interface), not to this service.\n\nMore info and examples at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage effect(leakingRequirements)"
41304
+ text: "Methods of this Service require `FileSystem` from every caller.\n\nThis leaks implementation details into the service's public type \u2014 callers shouldn't need to know *how* the service works internally, only *what* it provides.\n\nResolve these dependencies at Layer creation and provide them to each method, so the service's type reflects its purpose, not its implementation.\n\nTo suppress this diagnostic for specific dependency types that are intentionally passed through (e.g., HttpServerRequest), add `@effect-leakable-service` JSDoc to their interface declarations (e.g., the `FileSystem` interface), or to this service by adding a `@effect-expect-leaking FileSystem` JSDoc.\n\nMore info and examples at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage effect(leakingRequirements)"
41175
41305
  }
41176
41306
  ]
41177
41307
  }