@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 +4 -1
- package/cli.js +135 -5
- package/cli.js.map +1 -1
- package/effect-lsp-patch-utils.js +110 -3
- package/effect-lsp-patch-utils.js.map +1 -1
- package/index.js +110 -3
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/schema.json +2828 -0
- package/transform.js +110 -3
- package/transform.js.map +1 -1
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": "
|
|
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.
|
|
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
|
-
|
|
34070
|
-
|
|
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 \`${
|
|
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),
|
|
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
|
}
|