@effect/language-service 0.67.0 → 0.69.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
@@ -81,7 +81,9 @@ And you're done! You'll now be able to use a set of refactors and diagnostics th
81
81
  - Warn when using `Effect.fail` with the global `Error` type, recommending tagged errors
82
82
  - Warn when `Layer.mergeAll` contains layers with interdependencies (where one layer provides a service that another layer in the same call requires)
83
83
  - Suggest using `Effect.fn` for functions that return `Effect.gen` for better tracing and concise syntax
84
+ - Warn when `Effect.fn` or `Effect.fnUntraced` is used as an IIFE (Immediately Invoked Function Expression), suggesting `Effect.gen` instead
84
85
  - Suggest removing redundant identifier argument when it equals the tag value in `Schema.TaggedClass`, `Schema.TaggedError`, or `Schema.TaggedRequest`
86
+ - Suggest using `Schema.is` instead of `instanceof` for Effect Schema types
85
87
 
86
88
  ### Completions
87
89
 
package/cli.js CHANGED
@@ -26161,6 +26161,7 @@ var text7 = (name) => makeSingle(name, empty2(), text6);
26161
26161
  var getHelp3 = (self) => getHelpInternal2(self);
26162
26162
  var getUsage = (self) => getUsageInternal(self);
26163
26163
  var map29 = /* @__PURE__ */ dual(2, (self, f) => makeMap(self, (a) => right2(f(a))));
26164
+ var mapEffect5 = /* @__PURE__ */ dual(2, (self, f) => makeMap(self, f));
26164
26165
  var optional3 = (self) => withDefault2(map29(self, some2), none2());
26165
26166
  var orElse14 = /* @__PURE__ */ dual(2, (self, that) => orElseEither4(self, that).pipe(map29(merge)));
26166
26167
  var orElseEither4 = /* @__PURE__ */ dual(2, (self, that) => makeOrElse(self, that));
@@ -27122,6 +27123,7 @@ var directory2 = directory;
27122
27123
  var file3 = file2;
27123
27124
  var integer5 = integer4;
27124
27125
  var text8 = text7;
27126
+ var mapEffect6 = mapEffect5;
27125
27127
  var optional4 = optional3;
27126
27128
  var repeated4 = repeated3;
27127
27129
  var withDefault3 = withDefault2;
@@ -27173,8 +27175,8 @@ var none11 = /* @__PURE__ */ (() => {
27173
27175
  })();
27174
27176
  var getHelp4 = (self) => getHelpInternal3(self);
27175
27177
  var getUsage2 = (self) => getUsageInternal2(self);
27176
- var map30 = /* @__PURE__ */ dual(2, (self, f) => mapEffect6(self, (a) => succeed7(f(a))));
27177
- var mapEffect6 = /* @__PURE__ */ dual(2, (self, f) => makeMap2(self, f));
27178
+ var map30 = /* @__PURE__ */ dual(2, (self, f) => mapEffect7(self, (a) => succeed7(f(a))));
27179
+ var mapEffect7 = /* @__PURE__ */ dual(2, (self, f) => makeMap2(self, f));
27178
27180
  var validate5 = /* @__PURE__ */ dual(3, (self, args3, config2) => validateInternal2(self, args3, config2));
27179
27181
  var wizard3 = /* @__PURE__ */ dual(2, (self, config2) => wizardInternal3(self, config2));
27180
27182
  var allTupled2 = (arg) => {
@@ -27615,8 +27617,8 @@ var getFishCompletions4 = (self, executable) => getFishCompletionsInternal(self,
27615
27617
  var getZshCompletions4 = (self, executable) => getZshCompletionsInternal(self, executable);
27616
27618
  var getSubcommands = (self) => fromIterable6(getSubcommandsInternal(self));
27617
27619
  var getUsage3 = (self) => getUsageInternal3(self);
27618
- var map32 = /* @__PURE__ */ dual(2, (self, f) => mapEffect7(self, (a) => right2(f(a))));
27619
- var mapEffect7 = /* @__PURE__ */ dual(2, (self, f) => {
27620
+ var map32 = /* @__PURE__ */ dual(2, (self, f) => mapEffect8(self, (a) => right2(f(a))));
27621
+ var mapEffect8 = /* @__PURE__ */ dual(2, (self, f) => {
27620
27622
  const op = Object.create(proto23);
27621
27623
  op._tag = "Map";
27622
27624
  op.command = self;
@@ -27921,7 +27923,7 @@ var withDescriptionInternal = (self, description) => {
27921
27923
  return op;
27922
27924
  }
27923
27925
  case "Map": {
27924
- return mapEffect7(withDescriptionInternal(self.command, description), self.f);
27926
+ return mapEffect8(withDescriptionInternal(self.command, description), self.f);
27925
27927
  }
27926
27928
  case "Subcommands": {
27927
27929
  const op = Object.create(proto23);
@@ -28147,6 +28149,7 @@ var helpRequestedError = (command) => {
28147
28149
 
28148
28150
  // node_modules/.pnpm/@effect+cli@0.73.0_@effect+platform@0.94.1_@effect+printer-ansi@0.47.0_@effect+printer@0.47.0_effect@3.19.14/node_modules/@effect/cli/dist/esm/ValidationError.js
28149
28151
  var helpRequested = helpRequestedError;
28152
+ var invalidValue2 = invalidValue;
28150
28153
 
28151
28154
  // node_modules/.pnpm/effect@3.19.14/node_modules/effect/dist/esm/Logger.js
28152
28155
  var withMinimumLogLevel2 = withMinimumLogLevel;
@@ -30211,7 +30214,7 @@ var runMain3 = runMain2;
30211
30214
  // package.json
30212
30215
  var package_default = {
30213
30216
  name: "@effect/language-service",
30214
- version: "0.67.0",
30217
+ version: "0.69.0",
30215
30218
  packageManager: "pnpm@8.11.0",
30216
30219
  publishConfig: {
30217
30220
  access: "public",
@@ -36263,6 +36266,104 @@ ${versions.map((version) => `- found ${version} at ${resolvedPackages[packageNam
36263
36266
  })
36264
36267
  });
36265
36268
 
36269
+ // src/diagnostics/effectFnIife.ts
36270
+ var effectFnIife = createDiagnostic({
36271
+ name: "effectFnIife",
36272
+ code: 46,
36273
+ description: "Effect.fn or Effect.fnUntraced is called as an IIFE (Immediately Invoked Function Expression). Use Effect.gen instead.",
36274
+ severity: "warning",
36275
+ apply: fn2("effectFnIife.apply")(function* (sourceFile, report) {
36276
+ const ts = yield* service2(TypeScriptApi);
36277
+ const typeParser = yield* service2(TypeParser);
36278
+ const tsUtils = yield* service2(TypeScriptUtils);
36279
+ const sourceEffectModuleName = tsUtils.findImportedModuleIdentifierByPackageAndNameOrBarrel(
36280
+ sourceFile,
36281
+ "effect",
36282
+ "Effect"
36283
+ ) || "Effect";
36284
+ const nodeToVisit = [];
36285
+ const appendNodeToVisit = (node) => {
36286
+ nodeToVisit.push(node);
36287
+ return void 0;
36288
+ };
36289
+ ts.forEachChild(sourceFile, appendNodeToVisit);
36290
+ while (nodeToVisit.length > 0) {
36291
+ const node = nodeToVisit.shift();
36292
+ ts.forEachChild(node, appendNodeToVisit);
36293
+ if (!ts.isCallExpression(node)) continue;
36294
+ const innerCall = node.expression;
36295
+ if (!ts.isCallExpression(innerCall)) continue;
36296
+ const parsed = yield* pipe(
36297
+ typeParser.effectFnGen(innerCall),
36298
+ map34((result) => ({
36299
+ kind: "fn",
36300
+ effectModule: result.effectModule,
36301
+ generatorFunction: result.generatorFunction,
36302
+ pipeArguments: result.pipeArguments
36303
+ })),
36304
+ orElse15(
36305
+ () => pipe(
36306
+ typeParser.effectFnUntracedGen(innerCall),
36307
+ map34((result) => ({
36308
+ kind: "fnUntraced",
36309
+ effectModule: result.effectModule,
36310
+ generatorFunction: result.generatorFunction,
36311
+ pipeArguments: result.pipeArguments
36312
+ }))
36313
+ )
36314
+ ),
36315
+ orElse15(
36316
+ () => pipe(
36317
+ typeParser.effectFn(innerCall),
36318
+ map34((result) => ({
36319
+ kind: "fn",
36320
+ effectModule: result.effectModule,
36321
+ generatorFunction: void 0,
36322
+ pipeArguments: result.pipeArguments
36323
+ }))
36324
+ )
36325
+ ),
36326
+ option5
36327
+ );
36328
+ if (isNone2(parsed)) continue;
36329
+ const { effectModule, generatorFunction, kind, pipeArguments: pipeArguments2 } = parsed.value;
36330
+ const effectModuleName = ts.isIdentifier(effectModule) ? ts.idText(effectModule) : sourceEffectModuleName;
36331
+ const fixes = [];
36332
+ if (generatorFunction && generatorFunction.parameters.length === 0) {
36333
+ fixes.push({
36334
+ fixName: "effectFnIife_toEffectGen",
36335
+ description: "Convert to Effect.gen",
36336
+ apply: gen3(function* () {
36337
+ const changeTracker = yield* service2(ChangeTracker);
36338
+ const effectGenCall = ts.factory.createCallExpression(
36339
+ ts.factory.createPropertyAccessExpression(
36340
+ ts.factory.createIdentifier(effectModuleName),
36341
+ "gen"
36342
+ ),
36343
+ void 0,
36344
+ [generatorFunction]
36345
+ );
36346
+ let replacementNode = effectGenCall;
36347
+ if (pipeArguments2.length > 0) {
36348
+ replacementNode = ts.factory.createCallExpression(
36349
+ ts.factory.createPropertyAccessExpression(effectGenCall, "pipe"),
36350
+ void 0,
36351
+ [...pipeArguments2]
36352
+ );
36353
+ }
36354
+ changeTracker.replaceNode(sourceFile, node, replacementNode);
36355
+ })
36356
+ });
36357
+ }
36358
+ report({
36359
+ location: node,
36360
+ messageText: `${effectModuleName}.${kind} returns a reusable function that can take arguments, but here it's called immediately. Use Effect.gen instead (optionally with Effect.withSpan for tracing).`,
36361
+ fixes
36362
+ });
36363
+ }
36364
+ })
36365
+ });
36366
+
36266
36367
  // src/diagnostics/effectFnOpportunity.ts
36267
36368
  var effectFnOpportunity = createDiagnostic({
36268
36369
  name: "effectFnOpportunity",
@@ -37030,6 +37131,69 @@ var importFromBarrel = createDiagnostic({
37030
37131
  })
37031
37132
  });
37032
37133
 
37134
+ // src/diagnostics/instanceOfSchema.ts
37135
+ var instanceOfSchema = createDiagnostic({
37136
+ name: "instanceOfSchema",
37137
+ code: 45,
37138
+ description: "Suggests using Schema.is instead of instanceof for Effect Schema types",
37139
+ severity: "off",
37140
+ apply: fn2("instanceOfSchema.apply")(function* (sourceFile, report) {
37141
+ const ts = yield* service2(TypeScriptApi);
37142
+ const typeParser = yield* service2(TypeParser);
37143
+ const typeCheckerUtils = yield* service2(TypeCheckerUtils);
37144
+ const nodeToVisit = [];
37145
+ const appendNodeToVisit = (node) => {
37146
+ nodeToVisit.push(node);
37147
+ return void 0;
37148
+ };
37149
+ ts.forEachChild(sourceFile, appendNodeToVisit);
37150
+ while (nodeToVisit.length > 0) {
37151
+ const node = nodeToVisit.shift();
37152
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.InstanceOfKeyword) {
37153
+ const leftExpr = node.left;
37154
+ const rightExpr = node.right;
37155
+ const rightType = typeCheckerUtils.getTypeAtLocation(rightExpr);
37156
+ if (!rightType) {
37157
+ ts.forEachChild(node, appendNodeToVisit);
37158
+ continue;
37159
+ }
37160
+ const isSchemaType = yield* pipe(
37161
+ typeParser.effectSchemaType(rightType, rightExpr),
37162
+ option5
37163
+ );
37164
+ if (isSchemaType._tag === "Some") {
37165
+ report({
37166
+ location: node,
37167
+ messageText: "Consider using Schema.is instead of instanceof for Effect Schema types.",
37168
+ fixes: [{
37169
+ fixName: "instanceOfSchema_fix",
37170
+ description: "Replace with Schema.is",
37171
+ apply: gen3(function* () {
37172
+ const changeTracker = yield* service2(ChangeTracker);
37173
+ const schemaIsCall = ts.factory.createCallExpression(
37174
+ ts.factory.createPropertyAccessExpression(
37175
+ ts.factory.createIdentifier("Schema"),
37176
+ "is"
37177
+ ),
37178
+ void 0,
37179
+ [rightExpr]
37180
+ );
37181
+ const fullCall = ts.factory.createCallExpression(
37182
+ schemaIsCall,
37183
+ void 0,
37184
+ [leftExpr]
37185
+ );
37186
+ changeTracker.replaceNode(sourceFile, node, fullCall);
37187
+ })
37188
+ }]
37189
+ });
37190
+ }
37191
+ }
37192
+ ts.forEachChild(node, appendNodeToVisit);
37193
+ }
37194
+ })
37195
+ });
37196
+
37033
37197
  // src/diagnostics/layerMergeAllWithDependencies.ts
37034
37198
  var layerMergeAllWithDependencies = createDiagnostic({
37035
37199
  name: "layerMergeAllWithDependencies",
@@ -37237,12 +37401,18 @@ var leakingRequirements = createDiagnostic({
37237
37401
  );
37238
37402
  function reportLeakingRequirements(node, requirements) {
37239
37403
  if (requirements.length === 0) return;
37404
+ const requirementsStr = requirements.map((_) => typeChecker.typeToString(_)).join(" | ");
37240
37405
  report({
37241
37406
  location: node,
37242
- messageText: `This Service is leaking the ${requirements.map((_) => typeChecker.typeToString(_)).join(" | ")} requirement.
37243
- If these requirements cannot be cached and are expected to be provided per method invocation (e.g. HttpServerRequest), you can either safely disable this diagnostic for this line through quickfixes or mark the service declaration with a JSDoc @effect-leakable-service.
37244
- Services should usually be collected in the layer creation body, and then provided at each method that requires them.
37245
- More info at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage`,
37407
+ messageText: `Methods of this Service require \`${requirementsStr}\` from every caller.
37408
+
37409
+ This 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.
37410
+
37411
+ Resolve these dependencies at Layer creation and provide them to each method, so the service's type reflects its purpose, not its implementation.
37412
+
37413
+ 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.
37414
+
37415
+ More info and examples at https://effect.website/docs/requirements-management/layers/#avoiding-requirement-leakage`,
37246
37416
  fixes: []
37247
37417
  });
37248
37418
  }
@@ -39420,6 +39590,7 @@ var unsupportedServiceAccessors = createDiagnostic({
39420
39590
  // src/diagnostics.ts
39421
39591
  var diagnostics = [
39422
39592
  anyUnknownInErrorContext,
39593
+ instanceOfSchema,
39423
39594
  catchAllToMapError,
39424
39595
  catchUnfailableEffect,
39425
39596
  classSelfMismatch,
@@ -39460,6 +39631,7 @@ var diagnostics = [
39460
39631
  globalErrorInEffectFailure,
39461
39632
  layerMergeAllWithDependencies,
39462
39633
  effectMapVoid,
39634
+ effectFnIife,
39463
39635
  effectFnOpportunity,
39464
39636
  redundantSchemaTagIdentifier,
39465
39637
  schemaSyncInEffect,
@@ -41702,6 +41874,14 @@ var renderCodeActions = (result, assessmentState) => gen2(function* () {
41702
41874
  });
41703
41875
 
41704
41876
  // src/cli/quickfixes.ts
41877
+ var validDiagnosticNames = new Set(diagnostics.map((_) => _.name));
41878
+ var validDiagnosticCodes = new Set(diagnostics.map((_) => String(_.code)));
41879
+ var diagnosticCodeByName = new Map(diagnostics.map((_) => [_.name, _.code]));
41880
+ var ColumnRequiresLineError = class extends TaggedError("ColumnRequiresLineError") {
41881
+ get message() {
41882
+ return "The --column option requires --line to be specified.";
41883
+ }
41884
+ };
41705
41885
  var isSkipFix = (fixName) => fixName.endsWith("_skipNextLine") || fixName.endsWith("_skipFile");
41706
41886
  var renderQuickFix = (sourceFile, fix) => {
41707
41887
  const lines3 = [];
@@ -41767,9 +41947,42 @@ var quickfixes = make58(
41767
41947
  project: file3("project").pipe(
41768
41948
  optional4,
41769
41949
  withDescription3("The full path of the project tsconfig.json file to check for quick fixes.")
41950
+ ),
41951
+ code: text8("code").pipe(
41952
+ withDescription3("Filter by diagnostic name or code (e.g., 'floatingEffect' or '5')."),
41953
+ mapEffect6((value5) => {
41954
+ if (validDiagnosticNames.has(value5)) {
41955
+ return succeed7(diagnosticCodeByName.get(value5));
41956
+ }
41957
+ if (validDiagnosticCodes.has(value5)) {
41958
+ return succeed7(Number(value5));
41959
+ }
41960
+ const validValues = [...validDiagnosticNames].sort().join(", ");
41961
+ return fail7(
41962
+ invalidValue2(
41963
+ p2(`Invalid diagnostic code '${value5}'. Valid values: ${validValues}`)
41964
+ )
41965
+ );
41966
+ }),
41967
+ optional4
41968
+ ),
41969
+ line: integer5("line").pipe(
41970
+ withDescription3("Filter by line number (1-based)."),
41971
+ optional4
41972
+ ),
41973
+ column: integer5("column").pipe(
41974
+ withDescription3("Filter by column number (1-based). Requires --line to be specified."),
41975
+ optional4
41976
+ ),
41977
+ fix: text8("fix").pipe(
41978
+ withDescription3("Filter by fix name (e.g., 'floatingEffect_yieldStar')."),
41979
+ optional4
41770
41980
  )
41771
41981
  },
41772
- fn("quickfixes")(function* ({ file: file5, project: project3 }) {
41982
+ fn("quickfixes")(function* ({ code: code2, column: column3, file: file5, fix, line: line4, project: project3 }) {
41983
+ if (isSome2(column3) && isNone2(line4)) {
41984
+ return yield* new ColumnRequiresLineError();
41985
+ }
41773
41986
  const path2 = yield* Path2;
41774
41987
  const tsInstance = yield* TypeScriptContext;
41775
41988
  const filesToCheck = isSome2(project3) ? yield* getFileNamesInTsConfig(project3.value) : /* @__PURE__ */ new Set();
@@ -41813,6 +42026,12 @@ var quickfixes = make58(
41813
42026
  const diagnosticMap = /* @__PURE__ */ new Map();
41814
42027
  for (const diagnostic of result.diagnostics) {
41815
42028
  if (diagnostic.start === void 0) continue;
42029
+ if (isSome2(code2) && diagnostic.code !== code2.value) continue;
42030
+ if (isSome2(line4)) {
42031
+ const pos = tsInstance.getLineAndCharacterOfPosition(sourceFile, diagnostic.start);
42032
+ if (pos.line !== line4.value - 1) continue;
42033
+ if (isSome2(column3) && pos.character !== column3.value - 1) continue;
42034
+ }
41816
42035
  const key = `${diagnostic.start}-${diagnostic.start + (diagnostic.length ?? 0)}-${diagnostic.code}`;
41817
42036
  const ruleName = Object.values(diagnostics).find((_) => _.code === diagnostic.code)?.name ?? `unknown(${diagnostic.code})`;
41818
42037
  if (!diagnosticMap.has(key)) {
@@ -41834,6 +42053,7 @@ var quickfixes = make58(
41834
42053
  );
41835
42054
  for (const codeFix of result.codeFixes) {
41836
42055
  if (isSkipFix(codeFix.fixName)) continue;
42056
+ if (isSome2(fix) && codeFix.fixName !== fix.value) continue;
41837
42057
  const key = `${codeFix.start}-${codeFix.end}-${codeFix.code}`;
41838
42058
  const info2 = diagnosticMap.get(key);
41839
42059
  if (!info2) continue;