@effect/language-service 0.41.1 → 0.43.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
@@ -111,6 +111,7 @@ Few options can be provided alongside the initialization of the Language Service
111
111
  },
112
112
  "quickinfo": true, // controls Effect quickinfo (default: true)
113
113
  "quickinfoEffectParameters": "whenTruncated", // (default: "whenTruncated") controls when to display effect type parameters always,never,whenTruncated
114
+ "quickinfoMaximumLength": -1, // controls how long can be the types in the quickinfo hover (helps with very long type to improve perfs, defaults to -1 for no truncation, can be any number eg. 1000 and TS will try to fit as much as possible in that budget, higher number means more info.)
114
115
  "completions": true, // controls Effect completions (default: true)
115
116
  "goto": true, // controls Effect goto references (default: true)
116
117
  "inlays": true, // controls Effect provided inlayHints (default: true)
@@ -119,7 +120,8 @@ Few options can be provided alongside the initialization of the Language Service
119
120
  "namespaceImportPackages": [], // package names that should be preferred as imported with namespace imports e.g. ["effect", "@effect/*"] (default: [])
120
121
  "topLevelNamedReexports": "ignore", // for namespaceImportPackages, how should top level named re-exports (e.g. {pipe} from "effect") be treated? "ignore" will leave them as is, "follow" will rewrite them to the re-exported module (e.g. {pipe} from "effect/Function")
121
122
  "importAliases": { "Array": "Arr" }, // allows to chose some different names for import name aliases (only when not chosing to import the whole module) (default: {})
122
- "noExternal": false // disables features that provides links to external websites (such as links to mermaidchart.com) (default: false)
123
+ "noExternal": false, // disables features that provides links to external websites (such as links to mermaidchart.com) (default: false)
124
+ "keyPatterns": [{ "target": "service", "pattern": "default", "skipLeadingPath": ["src/"] }] // configure the key patterns; recommended reading more on the section "Configuring Key Patterns"
123
125
  }
124
126
  ]
125
127
  }
@@ -229,6 +231,56 @@ or you can set the severity for the entire project in the global plugin configur
229
231
  }
230
232
  ```
231
233
 
234
+ ## Configuring Key Patterns
235
+
236
+ Effect uses string keys for Services, Error Tags, RPC Methods, and more.
237
+ It can happen that sometimes, after some refactors or copy/paste, you may end up having wrong or non unique keys in your services.
238
+
239
+ To avoid that, the LSP suggests deterministic patterns for keys; that can be configured by the "keyPatterns" option.
240
+
241
+ To enable reporting of wrong or outdated keys, the rule "deterministicKeys" must be enabled first (off by default). To do so, adjust its diagnosticSeverity to error.
242
+
243
+ The keyPatterns key can then contain an array of the configured patterns.
244
+
245
+ ```jsonc
246
+ {
247
+ "compilerOptions": {
248
+ "plugins": [
249
+ {
250
+ "name": "@effect/language-service",
251
+ // ...
252
+ "diagnosticSeverity": {
253
+ "deterministicKeys": "error" // enables reporting of wrong keys
254
+ // ...
255
+ },
256
+ "keyPatterns": [
257
+ {
258
+ "target": "service", // what key type to target between service|error
259
+ "pattern": "default", // the chosen pattern
260
+ "skipLeadingPath": ["src/"] // other pattern specific configs
261
+ }
262
+ ]
263
+
264
+ }
265
+ ]
266
+ }
267
+ }
268
+ ```
269
+
270
+ ### Pattern: default
271
+
272
+ This pattern constructs keys by chaining package name + file path + class identifier.
273
+
274
+ E.g. `@effect/package/subpath-relative-to-package-root/FileName/ClassIdentifier`
275
+
276
+ If the filename and the class identifier are the same, they won't be repeated, but used only once.
277
+
278
+ The skipLeadingPath array can contain a set of prefixes to remove from the subpath part of the path. By default "src/" is removed for example.
279
+
280
+ ### Pattern: package-identifier
281
+
282
+ This pattern uses the package name + identifier. This usually works great if you have a flat structure, with one file per service/error.
283
+
232
284
  ## Known gotchas
233
285
 
234
286
  ### Svelte VSCode extension and SvelteKit
@@ -1174,6 +1174,7 @@ var defaults = {
1174
1174
  diagnosticSeverity: {},
1175
1175
  quickinfo: true,
1176
1176
  quickinfoEffectParameters: "whentruncated",
1177
+ quickinfoMaximumLength: -1,
1177
1178
  completions: true,
1178
1179
  goto: true,
1179
1180
  inlays: true,
@@ -1183,8 +1184,25 @@ var defaults = {
1183
1184
  barrelImportPackages: [],
1184
1185
  importAliases: {},
1185
1186
  renames: true,
1186
- noExternal: false
1187
+ noExternal: false,
1188
+ keyPatterns: [{
1189
+ target: "service",
1190
+ pattern: "default",
1191
+ skipLeadingPath: ["src/"]
1192
+ }]
1187
1193
  };
1194
+ function parseKeyPatterns(patterns) {
1195
+ const result = [];
1196
+ for (const entry of patterns) {
1197
+ if (!isObject(entry)) continue;
1198
+ result.push({
1199
+ target: hasProperty(entry, "target") && isString(entry.target) && ["service", "error"].includes(entry.target.toLowerCase()) ? entry.target.toLowerCase() : "service",
1200
+ pattern: hasProperty(entry, "pattern") && isString(entry.pattern) && ["package-identifier", "default"].includes(entry.pattern.toLowerCase()) ? entry.pattern.toLowerCase() : "default",
1201
+ skipLeadingPath: hasProperty(entry, "skipLeadingPath") && isArray(entry.skipLeadingPath) && entry.skipLeadingPath.every(isString) ? entry.skipLeadingPath : ["src/"]
1202
+ });
1203
+ }
1204
+ return result;
1205
+ }
1188
1206
  function parse(config) {
1189
1207
  return {
1190
1208
  refactors: isObject(config) && hasProperty(config, "refactors") && isBoolean(config.refactors) ? config.refactors : defaults.refactors,
@@ -1192,6 +1210,7 @@ function parse(config) {
1192
1210
  diagnosticSeverity: isObject(config) && hasProperty(config, "diagnosticSeverity") && isRecord(config.diagnosticSeverity) ? parseDiagnosticSeverity(config.diagnosticSeverity) : defaults.diagnosticSeverity,
1193
1211
  quickinfo: isObject(config) && hasProperty(config, "quickinfo") && isBoolean(config.quickinfo) ? config.quickinfo : defaults.quickinfo,
1194
1212
  quickinfoEffectParameters: isObject(config) && hasProperty(config, "quickinfoEffectParameters") && isString(config.quickinfoEffectParameters) && ["always", "never", "whentruncated"].includes(config.quickinfoEffectParameters.toLowerCase()) ? config.quickinfoEffectParameters.toLowerCase() : defaults.quickinfoEffectParameters,
1213
+ quickinfoMaximumLength: isObject(config) && hasProperty(config, "quickinfoMaximumLength") && isNumber(config.quickinfoMaximumLength) ? config.quickinfoMaximumLength : defaults.quickinfoMaximumLength,
1195
1214
  completions: isObject(config) && hasProperty(config, "completions") && isBoolean(config.completions) ? config.completions : defaults.completions,
1196
1215
  goto: isObject(config) && hasProperty(config, "goto") && isBoolean(config.goto) ? config.goto : defaults.goto,
1197
1216
  inlays: isObject(config) && hasProperty(config, "inlays") && isBoolean(config.inlays) ? config.inlays : defaults.inlays,
@@ -1201,7 +1220,8 @@ function parse(config) {
1201
1220
  importAliases: isObject(config) && hasProperty(config, "importAliases") && isRecord(config.importAliases) ? map2(config.importAliases, (value) => String(value)) : defaults.importAliases,
1202
1221
  topLevelNamedReexports: isObject(config) && hasProperty(config, "topLevelNamedReexports") && isString(config.topLevelNamedReexports) && ["ignore", "follow"].includes(config.topLevelNamedReexports.toLowerCase()) ? config.topLevelNamedReexports.toLowerCase() : defaults.topLevelNamedReexports,
1203
1222
  renames: isObject(config) && hasProperty(config, "renames") && isBoolean(config.renames) ? config.renames : defaults.renames,
1204
- noExternal: isObject(config) && hasProperty(config, "noExternal") && isBoolean(config.noExternal) ? config.noExternal : defaults.noExternal
1223
+ noExternal: isObject(config) && hasProperty(config, "noExternal") && isBoolean(config.noExternal) ? config.noExternal : defaults.noExternal,
1224
+ keyPatterns: isObject(config) && hasProperty(config, "keyPatterns") && isArray(config.keyPatterns) ? parseKeyPatterns(config.keyPatterns) : defaults.keyPatterns
1205
1225
  };
1206
1226
  }
1207
1227
 
@@ -3460,6 +3480,126 @@ var classSelfMismatch = createDiagnostic({
3460
3480
  })
3461
3481
  });
3462
3482
 
3483
+ // src/core/KeyBuilder.ts
3484
+ var makeKeyBuilder = fn("KeyBuilder")(
3485
+ function* (sourceFile) {
3486
+ const ts = yield* service(TypeScriptApi);
3487
+ const tsUtils = yield* service(TypeScriptUtils);
3488
+ const program = yield* service(TypeScriptProgram);
3489
+ const options = yield* service(LanguageServicePluginOptions);
3490
+ const packageInfo = tsUtils.resolveModuleWithPackageInfoFromSourceFile(program, sourceFile);
3491
+ function createString2(classNameText, kind) {
3492
+ if (!packageInfo) return;
3493
+ for (const keyPattern of options.keyPatterns) {
3494
+ if (keyPattern.target !== kind) continue;
3495
+ if (keyPattern.pattern === "package-identifier") {
3496
+ return packageInfo.name + "/" + classNameText;
3497
+ }
3498
+ const dirPath = getDirectoryPath(ts, sourceFile.fileName);
3499
+ if (!dirPath.startsWith(packageInfo.packageDirectory)) return;
3500
+ let subDirectory = dirPath.slice(packageInfo.packageDirectory.length);
3501
+ if (subDirectory.startsWith("/")) subDirectory = subDirectory.slice(1);
3502
+ const lastIndex = sourceFile.fileName.lastIndexOf("/");
3503
+ let subModule = lastIndex === -1 ? "" : sourceFile.fileName.slice(lastIndex + 1);
3504
+ for (const extension of [".ts", ".tsx", ".js", ".jsx"]) {
3505
+ if (subModule.toLowerCase().endsWith(extension)) {
3506
+ subModule = subModule.slice(0, -extension.length);
3507
+ break;
3508
+ }
3509
+ }
3510
+ if (subModule.toLowerCase().endsWith("/index")) subModule = subModule.slice(0, -6);
3511
+ if (subModule.startsWith("/")) subModule = subModule.slice(1);
3512
+ for (const prefix of keyPattern.skipLeadingPath) {
3513
+ if (subDirectory.startsWith(prefix)) {
3514
+ subDirectory = subDirectory.slice(prefix.length);
3515
+ break;
3516
+ }
3517
+ }
3518
+ const parts = [packageInfo.name, subDirectory, subModule].concat(
3519
+ subModule.toLowerCase() === classNameText.toLowerCase() ? [] : [classNameText]
3520
+ );
3521
+ return parts.filter((_) => String(_).trim().length > 0).join("/");
3522
+ }
3523
+ }
3524
+ return {
3525
+ createString: createString2
3526
+ };
3527
+ }
3528
+ );
3529
+ var keyBuilderCache = /* @__PURE__ */ new Map();
3530
+ var getOrMakeKeyBuilder = fn("getOrMakeKeyBuilder")(function* (sourceFile) {
3531
+ const keyBuilder = keyBuilderCache.get(sourceFile.fileName) || (yield* makeKeyBuilder(sourceFile));
3532
+ keyBuilderCache.set(sourceFile.fileName, keyBuilder);
3533
+ return keyBuilder;
3534
+ });
3535
+ function createString(sourceFile, identifier, kind) {
3536
+ return map4(
3537
+ getOrMakeKeyBuilder(sourceFile),
3538
+ (identifierBuilder) => identifierBuilder.createString(identifier, kind)
3539
+ );
3540
+ }
3541
+
3542
+ // src/diagnostics/deterministicKeys.ts
3543
+ var deterministicKeys = createDiagnostic({
3544
+ name: "deterministicKeys",
3545
+ code: 25,
3546
+ severity: "off",
3547
+ apply: fn("deterministicKeys.apply")(function* (sourceFile, report) {
3548
+ const ts = yield* service(TypeScriptApi);
3549
+ const typeParser = yield* service(TypeParser);
3550
+ const nodeToVisit = [];
3551
+ const appendNodeToVisit = (node) => {
3552
+ nodeToVisit.push(node);
3553
+ return void 0;
3554
+ };
3555
+ ts.forEachChild(sourceFile, appendNodeToVisit);
3556
+ while (nodeToVisit.length > 0) {
3557
+ const node = nodeToVisit.shift();
3558
+ if (ts.isClassDeclaration(node) && node.name && node.heritageClauses) {
3559
+ const result = yield* pipe(
3560
+ pipe(
3561
+ typeParser.extendsEffectService(node),
3562
+ orElse2(() => typeParser.extendsContextTag(node)),
3563
+ orElse2(() => typeParser.extendsEffectTag(node)),
3564
+ map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "service" }))
3565
+ ),
3566
+ orElse2(
3567
+ () => pipe(
3568
+ typeParser.extendsDataTaggedError(node),
3569
+ orElse2(() => typeParser.extendsSchemaTaggedError(node)),
3570
+ map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "error" }))
3571
+ )
3572
+ ),
3573
+ orElse2(() => void_)
3574
+ );
3575
+ if (result && result.keyStringLiteral) {
3576
+ const { className, keyStringLiteral } = result;
3577
+ const classNameText = ts.idText(className);
3578
+ const expectedKey = yield* createString(sourceFile, classNameText, result.target);
3579
+ if (!expectedKey) return;
3580
+ const actualIdentifier = keyStringLiteral.text;
3581
+ if (actualIdentifier !== expectedKey) {
3582
+ report({
3583
+ location: keyStringLiteral,
3584
+ messageText: `Key should be '${expectedKey}'`,
3585
+ fixes: [{
3586
+ fixName: "deterministicKeys_fix",
3587
+ description: `Replace '${actualIdentifier}' with '${expectedKey}'`,
3588
+ apply: gen(function* () {
3589
+ const changeTracker = yield* service(ChangeTracker);
3590
+ const newStringLiteral = ts.factory.createStringLiteral(expectedKey);
3591
+ changeTracker.replaceNode(sourceFile, keyStringLiteral, newStringLiteral);
3592
+ })
3593
+ }]
3594
+ });
3595
+ }
3596
+ }
3597
+ }
3598
+ ts.forEachChild(node, appendNodeToVisit);
3599
+ }
3600
+ })
3601
+ });
3602
+
3463
3603
  // src/diagnostics/duplicatePackage.ts
3464
3604
  var checkedPackagesCache = /* @__PURE__ */ new Map();
3465
3605
  var programResolvedCacheSize = /* @__PURE__ */ new Map();
@@ -5469,7 +5609,8 @@ var diagnostics = [
5469
5609
  outdatedEffectCodegen,
5470
5610
  overriddenSchemaConstructor,
5471
5611
  unsupportedServiceAccessors,
5472
- nonObjectEffectServiceType
5612
+ nonObjectEffectServiceType,
5613
+ deterministicKeys
5473
5614
  ];
5474
5615
 
5475
5616
  // src/effect-lsp-patch-utils.ts