Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/service-not-as-class-diagnostic.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@effect/language-service": minor
---

Add the `serviceNotAsClass` diagnostic to warn when `ServiceMap.Service` is used as a variable assignment instead of in a class declaration.

Includes an auto-fix that converts `const Config = ServiceMap.Service<Shape>("Config")` to `class Config extends ServiceMap.Service<Config, Shape>()("Config") {}`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ And you're done! You'll now be able to use a set of refactors and diagnostics th
- Suggest using `Schema.is` instead of `instanceof` for Effect Schema types
- Suggest using `Effect.void` instead of `Effect.succeed(undefined)` or `Effect.succeed(void 0)`
- Warn when using outdated Effect v3 APIs in an Effect v4 project, with guidance on the correct v4 replacement (renamed, changed, or removed APIs)
- Warn when `ServiceMap.Service` is used as a variable instead of a class declaration

### Completions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ exports[`Completion effectDataClasses > effectDataClasses_directImportTaggedErro
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
[
{
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics",
Expand All @@ -259,7 +259,7 @@ exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:
"sortText": "11",
},
{
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ exports[`Completion effectDataClasses > effectDataClasses.ts at 4:35 1`] = `
exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:5 1`] = `
[
{
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics",
Expand All @@ -154,7 +154,7 @@ exports[`Completion effectDiagnosticsComment > effectDiagnosticsComment.ts at 2:
"sortText": "11",
},
{
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"insertText": "@effect-diagnostics-next-line \${1|anyUnknownInErrorContext,catchAllToMapError,catchUnfailableEffect,classSelfMismatch,deterministicKeys,duplicatePackage,effectFnIife,effectFnOpportunity,effectGenUsesAdapter,effectInFailure,effectInVoidSuccess,effectMapVoid,effectSucceedWithVoid,extendsNativeError,floatingEffect,genericEffectServices,globalErrorInEffectCatch,globalErrorInEffectFailure,importFromBarrel,instanceOfSchema,layerMergeAllWithDependencies,leakingRequirements,missedPipeableOpportunity,missingEffectContext,missingEffectError,missingEffectServiceDependency,missingLayerContext,missingReturnYieldStar,missingStarInYieldEffectGen,multipleEffectProvide,nonObjectEffectServiceType,outdatedApi,outdatedEffectCodegen,overriddenSchemaConstructor,preferSchemaOverJson,redundantSchemaTagIdentifier,returnEffectInGen,runEffectInsideEffect,schemaStructWithTag,schemaSyncInEffect,schemaUnionOfLiterals,scopeInLayerEffect,serviceNotAsClass,strictBooleanExpressions,strictEffectProvide,tryCatchInEffectGen,unknownInEffectCatch,unnecessaryEffectGen,unnecessaryFailYieldableError,unnecessaryPipe,unnecessaryPipeChain,unsupportedServiceAccessors|}:\${2|off,warning,error,message,suggestion|}$0",
"isSnippet": true,
"kind": "string",
"name": "@effect-diagnostics-next-line",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
serviceNotAsClass from 180 to 223
serviceNotAsClass_skipNextLine from 180 to 223
serviceNotAsClass_skipFile from 180 to 223
serviceNotAsClass from 348 to 413
serviceNotAsClass_skipNextLine from 348 to 413
serviceNotAsClass_skipFile from 348 to 413
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ServiceMap.Service<ConfigService>("Config")
7:15 - 7:58 | 0 | ServiceMap.Service should be used in a class declaration instead of as a variable. Use: class Config extends ServiceMap.Service<Config, ConfigService>()("Config") {} effect(serviceNotAsClass)

ServiceMap.Service<ArtifactStoreService>("@my-app/ArtifactStore")
11:29 - 11:94 | 0 | ServiceMap.Service should be used in a class declaration instead of as a variable. Use: class ArtifactStore extends ServiceMap.Service<ArtifactStore, ArtifactStoreService>()("@my-app/ArtifactStore") {} effect(serviceNotAsClass)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// code fix serviceNotAsClass output for range 180 - 223
// @effect-diagnostics serviceNotAsClass:warning
import { ServiceMap } from "effect"

interface ConfigService {}

// Flagged: const variable with ServiceMap.Service
class Config extends ServiceMap.Service<Config, ConfigService>()("Config") { }

// Flagged: exported const variable with ServiceMap.Service
interface ArtifactStoreService {}
export const ArtifactStore = ServiceMap.Service<ArtifactStoreService>("@my-app/ArtifactStore")

// Not flagged: correct class extends form
class MyService extends ServiceMap.Service<MyService, { port: number }>()("MyService") {}

// Not flagged: non-ServiceMap.Service const
const x = 42
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// code fix serviceNotAsClass output for range 348 - 413
// @effect-diagnostics serviceNotAsClass:warning
import { ServiceMap } from "effect"

interface ConfigService {}

// Flagged: const variable with ServiceMap.Service
const Config = ServiceMap.Service<ConfigService>("Config")

// Flagged: exported const variable with ServiceMap.Service
interface ArtifactStoreService {}
export class ArtifactStore extends ServiceMap.Service<ArtifactStore, ArtifactStoreService>()("@my-app/ArtifactStore") { }

// Not flagged: correct class extends form
class MyService extends ServiceMap.Service<MyService, { port: number }>()("MyService") {}

// Not flagged: non-ServiceMap.Service const
const x = 42
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @effect-diagnostics serviceNotAsClass:warning
import { ServiceMap } from "effect"

interface ConfigService {}

// Flagged: const variable with ServiceMap.Service
const Config = ServiceMap.Service<ConfigService>("Config")

// Flagged: exported const variable with ServiceMap.Service
interface ArtifactStoreService {}
export const ArtifactStore = ServiceMap.Service<ArtifactStoreService>("@my-app/ArtifactStore")

// Not flagged: correct class extends form
class MyService extends ServiceMap.Service<MyService, { port: number }>()("MyService") {}

// Not flagged: non-ServiceMap.Service const
const x = 42
3 changes: 2 additions & 1 deletion packages/language-service/src/cli/layerinfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ function getExpressionName(tsApi: TypeScriptApi.TypeScriptApi, expr: ts.Expressi
return getExpressionName(tsApi, expr.expression)
}
// Fallback: truncate the text representation
const text = expr.getText().replace(/\s+/g, " ")
const sourceFile = expr.getSourceFile()
const text = sourceFile.text.substring(tsApi.getTokenPosOfNode(expr, sourceFile), expr.end).replace(/\s+/g, " ")
return text.length > 30 ? text.slice(0, 27) + "..." : text
}

Expand Down
5 changes: 4 additions & 1 deletion packages/language-service/src/cli/utils/ExportedSymbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ const getLocationFromDeclaration = (
): SymbolLocation | undefined => {
const sourceFile = declaration.getSourceFile()
if (!sourceFile) return undefined
const { character, line } = tsInstance.getLineAndCharacterOfPosition(sourceFile, declaration.getStart())
const { character, line } = tsInstance.getLineAndCharacterOfPosition(
sourceFile,
tsInstance.getTokenPosOfNode(declaration, sourceFile)
)
return {
filePath: sourceFile.fileName,
line: line + 1,
Expand Down
7 changes: 7 additions & 0 deletions packages/language-service/src/core/TypeScriptApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ declare module "typescript" {
/** @deprecated Use typeChecker.getSignaturesOfType(type, ts.SignatureKind.Construct) instead */
getConstructSignatures(): ReadonlyArray<ts.Signature>
}

export interface Node {
/** @deprecated Use ts.getTokenPosOfNode(node, sourceFile) instead */
getStart(sourceFile?: ts.SourceFile, includeJsDocComment?: boolean): number
/** @deprecated Use sourceFile.text.substring(node.pos, node.end) instead */
getText(sourceFile?: ts.SourceFile): string
}
}

type _TypeScriptApi = typeof ts
Expand Down
4 changes: 3 additions & 1 deletion packages/language-service/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { schemaStructWithTag } from "./diagnostics/schemaStructWithTag.js"
import { schemaSyncInEffect } from "./diagnostics/schemaSyncInEffect.js"
import { schemaUnionOfLiterals } from "./diagnostics/schemaUnionOfLiterals.js"
import { scopeInLayerEffect } from "./diagnostics/scopeInLayerEffect.js"
import { serviceNotAsClass } from "./diagnostics/serviceNotAsClass.js"
import { strictBooleanExpressions } from "./diagnostics/strictBooleanExpressions.js"
import { strictEffectProvide } from "./diagnostics/strictEffectProvide.js"
import { tryCatchInEffectGen } from "./diagnostics/tryCatchInEffectGen.js"
Expand Down Expand Up @@ -101,5 +102,6 @@ export const diagnostics = [
redundantSchemaTagIdentifier,
schemaSyncInEffect,
preferSchemaOverJson,
extendsNativeError
extendsNativeError,
serviceNotAsClass
]
Loading
Loading