@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/index.js CHANGED
@@ -7429,6 +7429,7 @@ var classSelfMismatch = createDiagnostic({
7429
7429
  if (ts.isClassDeclaration(node) && node.name && node.heritageClauses) {
7430
7430
  const result = yield* pipe(
7431
7431
  typeParser.extendsEffectService(node),
7432
+ orElse2(() => typeParser.extendsServiceMapService(node)),
7432
7433
  orElse2(() => typeParser.extendsContextTag(node)),
7433
7434
  orElse2(() => typeParser.extendsEffectTag(node)),
7434
7435
  orElse2(() => typeParser.extendsSchemaClass(node)),
@@ -7738,6 +7739,89 @@ var effectFnIife = createDiagnostic({
7738
7739
  })
7739
7740
  });
7740
7741
 
7742
+ // src/diagnostics/effectFnImplicitAny.ts
7743
+ var getParameterName = (typescript, name) => {
7744
+ if (typescript.isIdentifier(name)) {
7745
+ return typescript.idText(name);
7746
+ }
7747
+ return "parameter";
7748
+ };
7749
+ var hasOuterContextualFunctionType = (typescript, typeChecker, node) => {
7750
+ const contextualType = typeChecker.getContextualType(node);
7751
+ if (!contextualType) {
7752
+ return false;
7753
+ }
7754
+ return typeChecker.getSignaturesOfType(contextualType, typescript.SignatureKind.Call).length > 0;
7755
+ };
7756
+ var effectFnImplicitAny = createDiagnostic({
7757
+ name: "effectFnImplicitAny",
7758
+ code: 54,
7759
+ description: "Mirrors noImplicitAny for unannotated Effect.fn and Effect.fnUntraced callback parameters when no outer contextual function type exists",
7760
+ group: "correctness",
7761
+ severity: "error",
7762
+ fixable: false,
7763
+ supportedEffect: ["v3", "v4"],
7764
+ apply: fn("effectFnImplicitAny.apply")(function* (sourceFile, report) {
7765
+ const ts = yield* service(TypeScriptApi);
7766
+ const program = yield* service(TypeScriptProgram);
7767
+ const typeChecker = yield* service(TypeCheckerApi);
7768
+ const typeParser = yield* service(TypeParser);
7769
+ const noImplicitAny = program.getCompilerOptions().noImplicitAny ?? program.getCompilerOptions().strict ?? false;
7770
+ if (!noImplicitAny) {
7771
+ return;
7772
+ }
7773
+ const nodeToVisit = [sourceFile];
7774
+ const appendNodeToVisit = (node) => {
7775
+ nodeToVisit.push(node);
7776
+ return void 0;
7777
+ };
7778
+ while (nodeToVisit.length > 0) {
7779
+ const node = nodeToVisit.pop();
7780
+ ts.forEachChild(node, appendNodeToVisit);
7781
+ const parsed = yield* pipe(
7782
+ typeParser.effectFn(node),
7783
+ map5((result) => ({
7784
+ call: result.node,
7785
+ fn: result.regularFunction
7786
+ })),
7787
+ orElse2(
7788
+ () => pipe(
7789
+ typeParser.effectFnGen(node),
7790
+ map5((result) => ({
7791
+ call: result.node,
7792
+ fn: result.generatorFunction
7793
+ }))
7794
+ )
7795
+ ),
7796
+ orElse2(
7797
+ () => pipe(
7798
+ typeParser.effectFnUntracedGen(node),
7799
+ map5((result) => ({
7800
+ call: result.node,
7801
+ fn: result.generatorFunction
7802
+ }))
7803
+ )
7804
+ ),
7805
+ orUndefined
7806
+ );
7807
+ if (!parsed || hasOuterContextualFunctionType(ts, typeChecker, parsed.call)) {
7808
+ continue;
7809
+ }
7810
+ for (const parameter of parsed.fn.parameters) {
7811
+ if (parameter.type || parameter.initializer) {
7812
+ continue;
7813
+ }
7814
+ const parameterName = getParameterName(ts, parameter.name);
7815
+ report({
7816
+ location: parameter.name,
7817
+ messageText: `Parameter '${parameterName}' implicitly has an 'any' type in Effect.fn/Effect.fnUntraced. Add an explicit type annotation or provide a contextual function type.`,
7818
+ fixes: []
7819
+ });
7820
+ }
7821
+ }
7822
+ })
7823
+ });
7824
+
7741
7825
  // src/diagnostics/effectFnOpportunity.ts
7742
7826
  var effectFnOpportunity = createDiagnostic({
7743
7827
  name: "effectFnOpportunity",
@@ -9272,6 +9356,24 @@ var leakingRequirements = createDiagnostic({
9272
9356
  const typeCheckerUtils = yield* service(TypeCheckerUtils);
9273
9357
  const typeParser = yield* service(TypeParser);
9274
9358
  const tsUtils = yield* service(TypeScriptUtils);
9359
+ const isExpectedLeakingServiceSuppressed = (leakedServiceName, startNode) => {
9360
+ const sourceFile2 = tsUtils.getSourceFileOfNode(startNode);
9361
+ if (!sourceFile2) return false;
9362
+ return !!ts.findAncestor(startNode, (current) => {
9363
+ const ranges = ts.getLeadingCommentRanges(sourceFile2.text, current.pos) ?? [];
9364
+ const isSuppressed = ranges.some((range) => {
9365
+ const commentText = sourceFile2.text.slice(range.pos, range.end);
9366
+ return commentText.split("\n").filter((line) => line.includes("@effect-expect-leaking")).some((line) => {
9367
+ const markerIndex = line.indexOf("@effect-expect-leaking");
9368
+ return markerIndex !== -1 && line.slice(markerIndex + "@effect-expect-leaking".length).includes(leakedServiceName);
9369
+ });
9370
+ });
9371
+ if (isSuppressed) {
9372
+ return true;
9373
+ }
9374
+ return ts.isClassDeclaration(current) || ts.isVariableStatement(current) || ts.isExpressionStatement(current) || ts.isStatement(current) ? "quit" : false;
9375
+ });
9376
+ };
9275
9377
  const parseLeakedRequirements = cachedBy(
9276
9378
  fn("leakingServices.checkServiceLeaking")(
9277
9379
  function* (service2, atLocation) {
@@ -9353,8 +9455,12 @@ var leakingRequirements = createDiagnostic({
9353
9455
  (_, service2) => service2
9354
9456
  );
9355
9457
  function reportLeakingRequirements(node, requirements) {
9356
- if (requirements.length === 0) return;
9357
- const requirementsStr = requirements.map((_) => typeChecker.typeToString(_)).join(" | ");
9458
+ const filteredRequirements = requirements.filter(
9459
+ (requirement) => !isExpectedLeakingServiceSuppressed(typeChecker.typeToString(requirement), node)
9460
+ );
9461
+ if (filteredRequirements.length === 0) return;
9462
+ const requirementsStr = filteredRequirements.map((_) => typeChecker.typeToString(_)).join(" | ");
9463
+ const firstStr = typeChecker.typeToString(filteredRequirements[0]);
9358
9464
  report({
9359
9465
  location: node,
9360
9466
  messageText: `Methods of this Service require \`${requirementsStr}\` from every caller.
@@ -9363,7 +9469,7 @@ This leaks implementation details into the service's public type \u2014 callers
9363
9469
 
9364
9470
  Resolve these dependencies at Layer creation and provide them to each method, so the service's type reflects its purpose, not its implementation.
9365
9471
 
9366
- 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.
9472
+ 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.
9367
9473
 
9368
9474
  More info and examples at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage`,
9369
9475
  fixes: []
@@ -12615,6 +12721,7 @@ var diagnostics = [
12615
12721
  catchUnfailableEffect,
12616
12722
  classSelfMismatch,
12617
12723
  duplicatePackage,
12724
+ effectFnImplicitAny,
12618
12725
  effectGenUsesAdapter,
12619
12726
  missingEffectContext,
12620
12727
  missingEffectError,