diff --git a/.changeset/add-prefer-schema-union.md b/.changeset/add-prefer-schema-union.md new file mode 100644 index 00000000..d06af760 --- /dev/null +++ b/.changeset/add-prefer-schema-union.md @@ -0,0 +1,22 @@ +--- +"@effect/tsgo": minor +--- + +Add `preferSchemaUnion` diagnostic (`TS377130`) and code fix for Effect v4 to suggest composing Schema values with `Schema.Union` instead of unioning their extracted `Type` properties. + +### Example + +```ts +// Before +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export type Shape = typeof Circle.Type | typeof Square.Type + +// After applying code fix +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export const Shape = Schema.Union([Circle, Square]) +export type Shape = typeof Shape.Type +``` diff --git a/README.md b/README.md index b3778c5c..95100022 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,7 @@ Some diagnostics are off by default or have a default severity of suggestion, bu newSchemaClassSuggests using Schema make instead of new for Schema classes optionMatchToFromOptionSuggests Effect.fromOption when Option.match or an Option tag conditional only converts Some to Effect.succeed and None to Effect.fail preferSchemaTypePropertyDisallows Schema.Schema.Type<typeof X> in favor of typeof X.Type + preferSchemaUnionSuggests composing Schema values with Schema.Union instead of unioning their extracted Type properties preferSucceedSomeOrNoneSuggests using Effect.succeedNone or Effect.succeedSome instead of wrapping Option.none or Option.some with Effect.succeed preferTypedSchemaDecoderSuggests typed Schema decoders when the input is assignable to the schema's Encoded type provideLayerSucceedToProvideServiceSuggests providing inline Layer.succeed and Layer.effect services directly diff --git a/_packages/tsgo/src/metadata.json b/_packages/tsgo/src/metadata.json index b4483bac..7850afc3 100644 --- a/_packages/tsgo/src/metadata.json +++ b/_packages/tsgo/src/metadata.json @@ -2232,6 +2232,29 @@ ] } }, + { + "name": "preferSchemaUnion", + "group": "style", + "description": "Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties", + "defaultSeverity": "off", + "fixable": true, + "supportedEffect": [ + "v4" + ], + "codes": [ + 377130 + ], + "preview": { + "sourceText": "import { Schema } from \"effect\"\n\nexport const Circle = Schema.Struct({ kind: Schema.Literal(\"circle\"), radius: Schema.Number })\nexport const Square = Schema.Struct({ kind: Schema.Literal(\"square\"), side: Schema.Number })\n\nexport type Shape = typeof Circle.Type | typeof Square.Type\n", + "diagnostics": [ + { + "start": 242, + "end": 281, + "text": "This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)" + } + ] + } + }, { "name": "preferSucceedSomeOrNone", "group": "style", diff --git a/docs/rules/prefer-schema-union.md b/docs/rules/prefer-schema-union.md new file mode 100644 index 00000000..fab1e815 --- /dev/null +++ b/docs/rules/prefer-schema-union.md @@ -0,0 +1,66 @@ + + +# `preferSchemaUnion` + +Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties + +| Property | Value | +| --- | --- | +| Category | Style | +| Default severity | `off` | +| Fixable | Yes | +| Effect versions | v4 | +| Diagnostic codes | `TS377130` | +| Language Service name | `preferSchemaUnion` | +| Oxlint name | `effecttsgo/prefer-schema-union` | + +## Preview + +```ts +import { Schema } from "effect" + +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export type Shape = typeof Circle.Type | typeof Square.Type +/** + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ effecttsgo(prefer-schema-union): This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. +*/ +``` + +## Language Service Configuration + +See the [Language Service setup guide](../../README.md#installation) for installation instructions. + +```jsonc +{ + "$schema": "./node_modules/@effect/tsgo/schema.json", + "compilerOptions": { + "plugins": [ + { + "name": "@effect/language-service", + "diagnosticSeverity": { + "preferSchemaUnion": "warning" + } + } + ] + } +} +``` + +## Oxlint Configuration + +See the [Oxlint setup guide](../README.md#oxlint-setup) for installation and patching instructions. + +```json +{ + "$schema": "./node_modules/@effect/tsgo/oxlint-schema.json", + "options": { + "typeAware": true + }, + "plugins": ["effecttsgo"], + "rules": { + "effecttsgo/prefer-schema-union": "warn" + } +} +``` diff --git a/internal/diagnostics/effectDiagnosticMessages.json b/internal/diagnostics/effectDiagnosticMessages.json index 73c2c660..7ba4535e 100644 --- a/internal/diagnostics/effectDiagnosticMessages.json +++ b/internal/diagnostics/effectDiagnosticMessages.json @@ -506,5 +506,9 @@ "This module reference imports `{0}`, which is obsolete in Effect v4. In Effect v4, Schema is provided directly by `Schema` from `effect` (or `effect/Schema`). effect(obsoleteSchemaImport)": { "category": "Warning", "code": 377128 + }, + "This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion)": { + "category": "Suggestion", + "code": 377130 } } diff --git a/internal/fixables/fixables.go b/internal/fixables/fixables.go index 3966f6d1..ac3f2ffa 100644 --- a/internal/fixables/fixables.go +++ b/internal/fixables/fixables.go @@ -39,6 +39,7 @@ var All = []fixable.Fixable{ UnnecessaryArrowBlockFix, UnnecessaryTypeofTypeFix, PreferSchemaTypePropertyFix, + PreferSchemaUnionFix, EffectMapVoidFix, UnnecessaryFailYieldableErrorFix, ClassSelfMismatchFix, diff --git a/internal/fixables/prefer_schema_union.go b/internal/fixables/prefer_schema_union.go new file mode 100644 index 00000000..f5b4c79f --- /dev/null +++ b/internal/fixables/prefer_schema_union.go @@ -0,0 +1,343 @@ +package fixables + +import ( + "fmt" + "strings" + + "github.com/effect-ts/tsgo/internal/fixable" + "github.com/effect-ts/tsgo/internal/rewriter" + "github.com/effect-ts/tsgo/internal/rules" + "github.com/effect-ts/tsgo/internal/typeparser" + "github.com/microsoft/TypeScript/tsc/shim/ast" + "github.com/microsoft/TypeScript/tsc/shim/checker" + tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics" + "github.com/microsoft/TypeScript/tsc/shim/ls" + "github.com/microsoft/TypeScript/tsc/shim/scanner" +) + +var PreferSchemaUnionFix = fixable.Fixable{ + Name: "preferSchemaUnion", + Description: "Create Schema.Union and retain the type alias", + ErrorCodes: []int32{ + tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion.Code(), + }, + FixIDs: []string{"preferSchemaUnion_fix"}, + Run: runPreferSchemaUnionFix, +} + +func runPreferSchemaUnionFix(ctx *fixable.Context) []ls.CodeAction { + matches := rules.AnalyzePreferSchemaUnion(ctx.TypeParser, ctx.SourceFile) + for _, match := range matches { + if !match.Location.Intersects(ctx.Span) && !ctx.Span.ContainedBy(match.Location) { + continue + } + + if !isFixApplicable(ctx, match) { + continue + } + + action := ctx.NewFixAction(fixable.FixAction{ + Description: "Compose Schema.Union and derive type", + Run: func(tracker *rewriter.Tracker) { + applyPreferSchemaUnionFix(ctx, tracker, match) + }, + }) + if action != nil { + return []ls.CodeAction{*action} + } + return nil + } + + return nil +} + +func isFixApplicable(ctx *fixable.Context, match rules.PreferSchemaUnionMatch) bool { + if ctx == nil || ctx.SourceFile == nil || match.Alias == nil { + return false + } + + // Limit offered fixes to top-level aliases in external modules + if !ast.IsExternalModule(ctx.SourceFile) { + return false + } + if match.Alias.Parent == nil || match.Alias.Parent.Kind != ast.KindSourceFile { + return false + } + + aliasDecl := match.Alias.AsTypeAliasDeclaration() + if aliasDecl == nil || aliasDecl.Name() == nil || aliasDecl.Type == nil { + return false + } + + // No ambient declarations + if match.Alias.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(match.Alias) { + return false + } + + // No name collision for the new const in the value namespace + aliasName := aliasDecl.Name().Text() + if ctx.Checker.ResolveName(aliasName, match.Alias, ast.SymbolFlagsValue, false) != nil { + return false + } + + // No comments inside the replaced union range + typeNode := aliasDecl.Type.AsNode() + if hasCommentsInRange(ctx.SourceFile, typeNode.Pos(), typeNode.End()) { + return false + } + + // Schema leaves must be simple identifiers referencing same-file initialized const declarations or schema classes preceding the alias + if len(match.Schemas) == 0 { + return false + } + for _, schemaNode := range match.Schemas { + if schemaNode == nil || schemaNode.Kind != ast.KindIdentifier { + return false + } + + sym := ctx.Checker.GetSymbolAtLocation(schemaNode) + if sym == nil { + return false + } + if sym.Flags&ast.SymbolFlagsAlias != 0 { + return false + } + + if len(sym.Declarations) == 0 { + return false + } + + hasValidDecl := false + for _, decl := range sym.Declarations { + if decl == nil { + continue + } + if ast.GetSourceFileOfNode(decl) != ctx.SourceFile { + return false + } + // Must strictly precede the alias (no forward or self references) + if decl.End() > match.Alias.Pos() { + return false + } + // Must not be ambient + if decl.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(decl) { + return false + } + + if ast.IsVariableDeclaration(decl) { + vd := decl.AsVariableDeclaration() + if vd == nil || vd.Initializer == nil || !ast.IsVarConst(decl) { + return false + } + hasValidDecl = true + } else if ast.IsClassDeclaration(decl) { + hasValidDecl = true + } + } + + if !hasValidDecl { + return false + } + } + + return true +} + +func hasCommentsInRange(sf *ast.SourceFile, pos, end int) bool { + if sf == nil { + return false + } + text := sf.Text() + if pos < 0 || end > len(text) || pos >= end { + return false + } + rangeText := text[pos:end] + return strings.Contains(rangeText, "//") || strings.Contains(rangeText, "/*") +} + +func applyPreferSchemaUnionFix(ctx *fixable.Context, tracker *rewriter.Tracker, match rules.PreferSchemaUnionMatch) { + aliasDecl := match.Alias.AsTypeAliasDeclaration() + if aliasDecl == nil || aliasDecl.Name() == nil || aliasDecl.Type == nil { + return + } + aliasName := aliasDecl.Name().Text() + + // 1. Resolve Schema value import + schemaModuleName, needImport := resolveSchemaImport(ctx, match.Alias) + + if needImport { + var specifier *ast.Node + if schemaModuleName == "Schema" { + specifier = tracker.NewImportSpecifier(false, nil, tracker.NewIdentifier("Schema")) + } else { + specifier = tracker.NewImportSpecifier(false, tracker.NewIdentifier("Schema"), tracker.NewIdentifier(schemaModuleName)) + } + namedImports := tracker.NewNamedImports(tracker.NewNodeList([]*ast.Node{specifier})) + importClause := tracker.NewImportClause(ast.KindUnknown, nil, namedImports) + moduleSpec := tracker.NewStringLiteral("effect", ast.TokenFlagsNone) + importDecl := tracker.NewImportDeclaration(nil, importClause, moduleSpec, nil) + ast.SetParentInChildren(importDecl) + tracker.InsertAtTopOfFile(ctx.SourceFile, []*ast.Statement{importDecl}, false) + } + + // 2. Build Schema.Union([A, B]) + schemaId := tracker.NewIdentifier(schemaModuleName) + callee := tracker.NewPropertyAccessExpression( + schemaId, + nil, + tracker.NewIdentifier("Union"), + ast.NodeFlagsNone, + ) + + arrayElements := make([]*ast.Node, 0, len(match.Schemas)) + for _, schemaNode := range match.Schemas { + arrayElements = append(arrayElements, tracker.DeepCloneNode(schemaNode)) + } + arrayLiteral := tracker.NewArrayLiteralExpression(tracker.NewNodeList(arrayElements), false) + callExpr := tracker.NewCallExpression(callee, nil, nil, tracker.NewNodeList([]*ast.Node{arrayLiteral}), ast.NodeFlagsNone) + + // 3. Build const declaration + varDecl := tracker.NewVariableDeclaration(tracker.DeepCloneNode(aliasDecl.Name().AsNode()), nil, nil, callExpr) + varDeclList := tracker.NewVariableDeclarationList(tracker.NewNodeList([]*ast.Node{varDecl}), ast.NodeFlagsConst) + + var modifierList *ast.ModifierList + if ast.HasSyntacticModifier(match.Alias, ast.ModifierFlagsExport) { + modifierList = tracker.NewModifierList([]*ast.Node{tracker.NewModifier(ast.KindExportKeyword)}) + } + constStatement := tracker.NewVariableStatement(modifierList, varDeclList) + ast.SetParentInChildren(constStatement) + + // 4. Insert const immediately before the alias + tracker.InsertNodeBefore(ctx.SourceFile, match.Alias, constStatement, false, rewriter.LeadingTriviaOptionNone) + + // 5. Replace RHS of alias with typeof Alias.Type + schemaType := tracker.NewQualifiedName(tracker.NewIdentifier(aliasName), tracker.NewIdentifier("Type")) + rhsReplacement := tracker.NewTypeQueryNode(schemaType, nil) + ast.SetParentInChildren(rhsReplacement) + tracker.ReplaceNode(ctx.SourceFile, aliasDecl.Type.AsNode(), rhsReplacement, nil) +} + +func resolveSchemaImport(ctx *fixable.Context, aliasNode *ast.Node) (string, bool) { + candidate := typeparser.FindModuleIdentifier(ctx.SourceFile, "Schema") + if isUsableSchemaImport(ctx, candidate) { + return candidate, false + } + return chooseUnboundSchemaName(ctx.Checker, aliasNode), true +} + +func isUsableSchemaImport(ctx *fixable.Context, candidate string) bool { + if ctx == nil || ctx.SourceFile == nil || candidate == "" { + return false + } + for _, stmt := range ctx.SourceFile.Statements.Nodes { + if stmt.Kind != ast.KindImportDeclaration { + continue + } + importDecl := stmt.AsImportDeclaration() + if importDecl == nil || importDecl.ModuleSpecifier == nil || importDecl.ImportClause == nil { + continue + } + if importDecl.AsNode().IsTypeOnly() { + continue + } + clauseNode := importDecl.ImportClause.AsNode() + if clauseNode.IsTypeOnly() { + continue + } + clause := importDecl.ImportClause.AsImportClause() + if clause == nil { + continue + } + + moduleName := scanner.GetTextOfNode(importDecl.ModuleSpecifier) + if len(moduleName) >= 2 && (moduleName[0] == '"' || moduleName[0] == '\'') { + moduleName = moduleName[1 : len(moduleName)-1] + } + + if clause.NamedBindings == nil { + continue + } + + if moduleName == "effect/Schema" && clause.NamedBindings.Kind == ast.KindNamespaceImport { + nsImport := clause.NamedBindings.AsNamespaceImport() + if nsImport != nil && nsImport.Name() != nil && nsImport.Name().Text() == candidate { + if !ast.IsTypeOnlyImportDeclaration(nsImport.AsNode()) { + if isSymbolFromEffectPackage(ctx, nsImport.Name().AsNode()) { + return true + } + } + } + } + + if (moduleName == "effect" || moduleName == "effect/Schema") && clause.NamedBindings.Kind == ast.KindNamedImports { + namedImports := clause.NamedBindings.AsNamedImports() + if namedImports == nil || namedImports.Elements == nil { + continue + } + for _, elem := range namedImports.Elements.Nodes { + spec := elem.AsImportSpecifier() + if spec == nil || ast.IsTypeOnlyImportDeclaration(elem) { + continue + } + localName := spec.Name().Text() + importedName := localName + if spec.PropertyName != nil { + importedName = spec.PropertyName.Text() + } + if localName == candidate && importedName == "Schema" { + if isSymbolFromEffectPackage(ctx, spec.Name().AsNode()) { + return true + } + } + } + } + } + return false +} + +func chooseUnboundSchemaName(c *checker.Checker, aliasNode *ast.Node) string { + if !isNameBound(c, aliasNode, "Schema") { + return "Schema" + } + name := "SchemaUnion" + suffix := 2 + for isNameBound(c, aliasNode, name) { + name = fmt.Sprintf("SchemaUnion%d", suffix) + suffix++ + } + return name +} + +func isNameBound(c *checker.Checker, location *ast.Node, name string) bool { + if c == nil || location == nil { + return false + } + if c.ResolveName(name, location, ast.SymbolFlagsValue, false) != nil { + return true + } + if c.ResolveName(name, location, ast.SymbolFlagsType, false) != nil { + return true + } + return false +} + +func isSymbolFromEffectPackage(ctx *fixable.Context, node *ast.Node) bool { + if ctx == nil || ctx.TypeParser == nil || ctx.Checker == nil || node == nil { + return false + } + sym := ctx.Checker.GetSymbolAtLocation(node) + if sym == nil { + return false + } + for sym != nil && sym.Flags&ast.SymbolFlagsAlias != 0 { + sym = ctx.Checker.GetAliasedSymbol(sym) + } + if sym == nil || len(sym.Declarations) == 0 { + return false + } + declSf := ast.GetSourceFileOfNode(sym.Declarations[0]) + if declSf == nil { + return false + } + return ctx.TypeParser.IsSourceFileInPackage(declSf, "effect") +} diff --git a/internal/rules/prefer_schema_union.go b/internal/rules/prefer_schema_union.go new file mode 100644 index 00000000..37de5e97 --- /dev/null +++ b/internal/rules/prefer_schema_union.go @@ -0,0 +1,181 @@ +package rules + +import ( + "github.com/effect-ts/tsgo/etscore" + "github.com/effect-ts/tsgo/internal/rule" + "github.com/effect-ts/tsgo/internal/typeparser" + "github.com/microsoft/TypeScript/tsc/shim/ast" + "github.com/microsoft/TypeScript/tsc/shim/checker" + "github.com/microsoft/TypeScript/tsc/shim/core" + tsdiag "github.com/microsoft/TypeScript/tsc/shim/diagnostics" + "github.com/microsoft/TypeScript/tsc/shim/scanner" +) + +var PreferSchemaUnion = rule.Rule{ + Name: "preferSchemaUnion", + Group: "style", + Description: "Suggests composing Schema values with Schema.Union instead of unioning their extracted Type properties", + DefaultSeverity: etscore.SeverityOff, + SupportedEffect: []string{"v4"}, + Codes: []int32{ + tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion.Code(), + }, + Run: func(ctx *rule.Context) []*ast.Diagnostic { + matches := AnalyzePreferSchemaUnion(ctx.TypeParser, ctx.SourceFile) + diags := make([]*ast.Diagnostic, len(matches)) + for i, match := range matches { + diags[i] = ctx.NewDiagnostic( + match.SourceFile, + match.Location, + tsdiag.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion, + nil, + ) + } + return diags + }, +} + +type PreferSchemaUnionMatch struct { + SourceFile *ast.SourceFile + Location core.TextRange + Alias *ast.Node + Schemas []*ast.Node +} + +func AnalyzePreferSchemaUnion(tp *typeparser.TypeParser, sf *ast.SourceFile) []PreferSchemaUnionMatch { + if tp == nil || sf == nil || sf.IsDeclarationFile { + return nil + } + if sf.ScriptKind != core.ScriptKindTS && sf.ScriptKind != core.ScriptKindTSX && sf.ScriptKind != core.ScriptKindUnknown { + return nil + } + if tp.DetectEffectVersion() != typeparser.EffectMajorV4 { + return nil + } + + var matches []PreferSchemaUnionMatch + var walk ast.Visitor + walk = func(node *ast.Node) bool { + if node == nil { + return false + } + if node.Kind == ast.KindTypeAliasDeclaration { + if match := analyzePreferSchemaUnionAlias(tp, sf, node); match != nil { + matches = append(matches, *match) + } + } + node.ForEachChild(walk) + return false + } + walk(sf.AsNode()) + return matches +} + +func analyzePreferSchemaUnionAlias(tp *typeparser.TypeParser, sf *ast.SourceFile, node *ast.Node) *PreferSchemaUnionMatch { + if node.Flags&ast.NodeFlagsAmbient != 0 || ast.HasAmbientModifier(node) { + return nil + } + + ta := node.AsTypeAliasDeclaration() + if ta == nil || ta.Type == nil { + return nil + } + if ta.TypeParameters != nil && len(ta.TypeParameters.Nodes) > 0 { + return nil + } + + unwrapped := ast.SkipTypeParentheses(ta.Type.AsNode()) + if unwrapped == nil || unwrapped.Kind != ast.KindUnionType { + return nil + } + + leaves := collectUnionLeaves(unwrapped) + if len(leaves) < 2 { + return nil + } + + schemas := make([]*ast.Node, 0, len(leaves)) + for _, leaf := range leaves { + if leaf.Kind != ast.KindTypeQuery { + return nil + } + query := leaf.AsTypeQueryNode() + if query == nil || query.ExprName == nil { + return nil + } + if query.TypeArguments != nil && len(query.TypeArguments.Nodes) > 0 { + return nil + } + + exprName := query.ExprName.AsNode() + if exprName == nil || exprName.Kind != ast.KindQualifiedName { + return nil + } + qual := exprName.AsQualifiedName() + if qual.Right == nil || qual.Right.Text() != "Type" || qual.Left == nil { + return nil + } + + schemaNode := qual.Left.AsNode() + t := getSchemaNodeType(tp, schemaNode) + if t == nil || t.Flags()&checker.TypeFlagsAnyOrUnknown != 0 { + return nil + } + if !tp.IsSchemaType(t) { + return nil + } + + schemas = append(schemas, schemaNode) + } + + return &PreferSchemaUnionMatch{ + SourceFile: sf, + Location: scanner.GetErrorRangeForNode(sf, ta.Type.AsNode()), + Alias: node, + Schemas: schemas, + } +} + +func collectUnionLeaves(node *ast.Node) []*ast.Node { + node = ast.SkipTypeParentheses(node) + if node == nil { + return nil + } + if node.Kind == ast.KindUnionType { + ut := node.AsUnionTypeNode() + if ut == nil || ut.Types == nil { + return nil + } + var leaves []*ast.Node + for _, child := range ut.Types.Nodes { + childLeaves := collectUnionLeaves(child) + if childLeaves == nil { + return nil + } + leaves = append(leaves, childLeaves...) + } + return leaves + } + return []*ast.Node{node} +} + +func getSchemaNodeType(tp *typeparser.TypeParser, node *ast.Node) *checker.Type { + if tp == nil || node == nil { + return nil + } + t := tp.GetTypeAtLocation(node) + if t != nil && t.Flags()&checker.TypeFlagsAnyOrUnknown == 0 { + return t + } + sym := tp.GetSymbolAtLocation(node) + if sym == nil && node.Kind == ast.KindQualifiedName { + qual := node.AsQualifiedName() + if qual.Right != nil { + sym = tp.GetSymbolAtLocation(qual.Right.AsNode()) + } + } + if sym != nil { + return tp.GetTypeOfSymbolAtLocation(sym, node) + } + return nil +} diff --git a/internal/rules/rules.go b/internal/rules/rules.go index ccaa3bee..b666223f 100644 --- a/internal/rules/rules.go +++ b/internal/rules/rules.go @@ -86,6 +86,7 @@ var All = []rule.Rule{ UnnecessaryArrowBlock, UnnecessaryTypeofType, PreferSchemaTypeProperty, + PreferSchemaUnion, InstanceOfSchema, GenericEffectServices, OverriddenSchemaConstructor, diff --git a/internal/typeparser/helpers.go b/internal/typeparser/helpers.go index 9f77237f..d57a0af3 100644 --- a/internal/typeparser/helpers.go +++ b/internal/typeparser/helpers.go @@ -82,6 +82,13 @@ func (tp *TypeParser) GetSymbolAtLocation(node *ast.Node) *ast.Symbol { return tp.checker.GetSymbolAtLocation(node) } +func (tp *TypeParser) GetTypeOfSymbolAtLocation(sym *ast.Symbol, node *ast.Node) *checker.Type { + if tp == nil || tp.checker == nil || sym == nil { + return nil + } + return tp.checker.GetTypeOfSymbolAtLocation(sym, node) +} + func (tp *TypeParser) resolveAliasedSymbol(sym *ast.Symbol) *ast.Symbol { if tp == nil || tp.checker == nil { return sym diff --git a/internal/typeparser/schema_type.go b/internal/typeparser/schema_type.go index e00b5b07..f938876c 100644 --- a/internal/typeparser/schema_type.go +++ b/internal/typeparser/schema_type.go @@ -110,6 +110,11 @@ func (tp *TypeParser) IsNodeReferenceToEffectSchemaModuleApi(node *ast.Node, mem return tp.IsNodeReferenceToModuleExport(node, effectSchemaModuleDescriptor, memberName) } +// IsNodeReferenceToEffectSchemaModule reports whether node resolves to the Effect Schema module. +func (tp *TypeParser) IsNodeReferenceToEffectSchemaModule(node *ast.Node) bool { + return tp.IsNodeReferenceToModule(node, effectSchemaModuleDescriptor) +} + func isParseResultSourceFile(_ *TypeParser, c *checker.Checker, sf *ast.SourceFile) bool { if c == nil || sf == nil { return false diff --git a/shim/diagnostics/shim.go b/shim/diagnostics/shim.go index f93ae1fd..8b6d0bac 100644 --- a/shim/diagnostics/shim.go +++ b/shim/diagnostics/shim.go @@ -1922,6 +1922,7 @@ var This_syntax_is_reserved_in_files_with_the_mts_or_cts_extension_Use_an_as_exp var This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found = diagnostics.This_syntax_requires_an_imported_helper_but_module_0_cannot_be_found var This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0 = diagnostics.This_syntax_requires_an_imported_helper_named_1_which_does_not_exist_in_0_Consider_upgrading_your_version_of_0 var This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0 = diagnostics.This_syntax_requires_an_imported_helper_named_1_with_2_parameters_which_is_not_compatible_with_the_one_in_0_Consider_upgrading_your_version_of_0 +var This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion = diagnostics.This_type_alias_unions_decoded_Effect_Schema_types_Prefer_Schema_Union_and_derive_the_type_from_the_resulting_schema_effect_preferSchemaUnion var This_type_assertion_unsafely_narrows_the_error_or_requirements_channels_effect_unsafeEffectTypeAssertion = diagnostics.This_type_assertion_unsafely_narrows_the_error_or_requirements_channels_effect_unsafeEffectTypeAssertion var This_type_parameter_might_need_an_extends_0_constraint = diagnostics.This_type_parameter_might_need_an_extends_0_constraint var This_typeof_Type_query_can_be_replaced_with_0_effect_unnecessaryTypeofType = diagnostics.This_typeof_Type_query_can_be_replaced_with_0_effect_unnecessaryTypeofType diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt new file mode 100644 index 00000000..a0fe4aef --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.errors.txt @@ -0,0 +1,16 @@ +=== Metadata === +Effect version: 3.19.19 + + + +==== /.src/preferSchemaUnion.ts (0 errors) ==== + // @effect-v3 + // @effect-diagnostics *:off + // @effect-diagnostics preferSchemaUnion:warning + import { Schema } from "effect" + + const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) + const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + + export type Shape = typeof Circle.Type | typeof Square.Type + diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid new file mode 100644 index 00000000..6ffd975e --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.preferSchemaUnion.mermaid @@ -0,0 +1,5 @@ +flowchart TB + 0[/"type: Struct#lt;#123; kind: Literal#lt;#91;#quot;circle#quot;#93;#gt;; radius: typeof Number$; #125;#gt;
node: Schema.Struct#40;#123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;#41;"/] + 1[/"type: Struct#lt;#123; kind: Literal#lt;#91;#quot;square#quot;#93;#gt;; side: typeof Number$; #125;#gt;
node: Schema.Struct#40;#123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;#41;"/] + 2[/"type:
node: Circle.Type"/] + 3[/"type:
node: Square.Type"/] \ No newline at end of file diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt new file mode 100644 index 00000000..05b8fe3e --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.flows.txt @@ -0,0 +1 @@ +/.src/preferSchemaUnion.ts -> preferSchemaUnion.flows.preferSchemaUnion.mermaid diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt new file mode 100644 index 00000000..cfd7e2d8 --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.layers.txt @@ -0,0 +1 @@ +==== /.src/preferSchemaUnion.ts (0 layer exports) ==== diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt new file mode 100644 index 00000000..df01f611 --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.pipings.txt @@ -0,0 +1,57 @@ +==== /.src/preferSchemaUnion.ts (4 flows) ==== + +=== Piping Flow === +Location: 6:15 - 6:88 +Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("circle"), radius: Schema.Number } +Subject Type: { kind: Literal<["circle"]>; radius: typeof Number$; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ kind: Literal<["circle"]>; radius: typeof Number$; }> + +=== Piping Flow === +Location: 6:37 - 6:62 +Node: Schema.Literal("circle") +Node Kind: KindCallExpression + +Subject: "circle" +Subject Type: "circle" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<["circle"]> + +=== Piping Flow === +Location: 7:15 - 7:86 +Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("square"), side: Schema.Number } +Subject Type: { kind: Literal<["square"]>; side: typeof Number$; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ kind: Literal<["square"]>; side: typeof Number$; }> + +=== Piping Flow === +Location: 7:37 - 7:62 +Node: Schema.Literal("square") +Node Kind: KindCallExpression + +Subject: "square" +Subject Type: "square" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<["square"]> diff --git a/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt b/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt new file mode 100644 index 00000000..dad05bfc --- /dev/null +++ b/testdata/baselines/reference/effect-v3/preferSchemaUnion.quickfixes.txt @@ -0,0 +1,5 @@ +=== Quick Fix Inventory === +(no diagnostics) + +=== Quick Fix Application Results === +(no quick fixes to apply) diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt new file mode 100644 index 00000000..c54f12d7 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.errors.txt @@ -0,0 +1,99 @@ +=== Metadata === +Effect version: 4.0.0 + +/.src/preferSchemaUnion.ts(10,22): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(11,22): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(21,29): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(44,30): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(46,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(53,31): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(56,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(60,3): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) +/.src/preferSchemaUnion.ts(65,28): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + +==== /.src/preferSchemaUnion.ts (9 errors) ==== + // @effect-diagnostics *:off + // @effect-diagnostics preferSchemaUnion:warning + import { Schema, Schema as S } from "effect" + + // 1. Two and three concrete schema leaves, including parenthesized unions + export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) + export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + + export type Shape2 = typeof Circle.Type | typeof Square.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + // 2. Schema.Class, refinements and transformed codecs + export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String + }) {} + export const NonEmptyString = Schema.NonEmptyString + export const NumberFromString = Schema.NumberFromString + + export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + // 3. Negative cases - NO DIAGNOSTIC + type MixedPrimitive = typeof Circle.Type | null + type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded + const Lookalike = { Type: "lookalike" as const } + type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type + declare const AnyReceiver: any + type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type + type GenericAlias = typeof Circle.Type | typeof Square.Type + declare type AmbientAlias = typeof Circle.Type | typeof Square.Type + type SingleMember = typeof Circle.Type + type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string + } + declare const Narrowed: NarrowedSchemaWithoutEncoded + type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type + export const AlreadyComposed = Schema.Union([Circle, Square]) + export type AlreadyComposed = typeof AlreadyComposed.Type + + // 4. Diagnosed, but safety checks withhold code action + export const CollidingValue = 42 + export type CollidingValue = typeof Circle.Type | typeof Square.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + export const ForwardA = Schema.Struct({ a: Schema.String }) + export const ForwardB = Schema.Struct({ b: Schema.String }) + + namespace Models { + export const SubItem = Schema.String + } + export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + let MutableSchema = Schema.Struct({ m: Schema.String }) + export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + // 5. Existing aliased Schema import + export const A = S.Struct({ a: S.String }) + export const B = S.Struct({ b: S.Number }) + export type AliasedUnion = typeof A.Type | typeof B.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid new file mode 100644 index 00000000..76b0ae0d --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.preferSchemaUnion.mermaid @@ -0,0 +1,109 @@ +flowchart TB + 0[/"type: #123; kind: Literal#lt;#quot;circle#quot;#gt;; radius: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;"/] + 1[/"type: #quot;circle#quot;
node: #quot;circle#quot;"/] + 2["type: Literal#lt;#quot;circle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"] + 3[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/] + 4["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 5[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 6[/"type: #123; kind: Literal#lt;#quot;square#quot;#gt;; side: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;"/] + 7[/"type: #quot;square#quot;
node: #quot;square#quot;"/] + 8["type: Literal#lt;#quot;square#quot;#gt;
callee: Schema.Literal
args: #91;#93;"] + 9[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/] + 10["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 11[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 12[/"type: #123; kind: Literal#lt;#quot;triangle#quot;#gt;; base: Number; height: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;triangle#quot;#41;, base: Schema.Number, height: Schema.Number #125;"/] + 13[/"type: #quot;triangle#quot;
node: #quot;triangle#quot;"/] + 14["type: Literal#lt;#quot;triangle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"] + 15[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/] + 16["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;triangle#quot;#gt;; readonly base: Number; readonly height: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 17[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 18[/"type:
node: Circle.Type"/] + 19[/"type:
node: Square.Type"/] + 20[/"type:
node: Circle.Type"/] + 21[/"type:
node: Square.Type"/] + 22[/"type:
node: Triangle.Type"/] + 23[/"type: Class#lt;UserClass, Struct#lt;#123; readonly id: String; readonly name: String; #125;#gt;, #123;#125;#gt;
node: Schema.Class#lt;UserClass#gt;#40;#quot;UserClass#quot;#41;#40;#123;#92;n id: Schema.String,#92;n name: Schema.String#92;n#125;#41;"/] + 24[/"type: #quot;UserClass#quot;
node: #quot;UserClass#quot;"/] + 25["type: #123; #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields, annotations?: Declaration#lt;UserClass, readonly #91;Struct#lt;Fields#gt;#93;#gt; #124; undefined#41;: Class#lt;UserClass, Struct#lt;Fields#gt;, #123;#125;#gt;; #lt;S extends Struct#lt;Struct.Fields#gt;#gt;#40;schema: S, annotations?: Declaration#lt;UserClass, readonly #91;S#93;#gt; #124; undefined#41;: Class#lt;UserClass, S, #123;#125;#gt;; #125;
callee: Schema.Class
args: #91;#93;"] + 26[/"type: #lt;Self = never, Brand = #123;#125;#gt;#40;identifier: string#41; =#gt; #123; #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields, annotations?: Declaration#lt;Self, readonly #91;Struct#lt;Fields#gt;#93;#gt; #124; undefined#41;: #91;Self#93; extends #91;never#93; ? #quot;Missing `Self` generic - use `class Self extends Schema.Class#lt;Self#gt;#40;...#41;`#quot; : Class#lt;Self, Struct#lt;Fields#gt;, Brand#gt;; #lt;S extends Struct#lt;Struct.Fields#gt;#gt;#40;schema: S, annotations?: Declaration#lt;Self, readonly #91;S#93;#gt; #124; undefined#41;: #91;Self#93; extends #91;never#93; ? #quot;Missing `Self` generic - use `class Self extends Schema.Class#lt;Self#gt;#40;...#41;`#quot; : Class#lt;Self, S, Brand#gt;; #125;
node: Schema.Class"/] + 27[/"type: NonEmptyString
node: Schema.NonEmptyString"/] + 28[/"type: NumberFromString
node: Schema.NumberFromString"/] + 29[/"type:
node: UserClass.Type"/] + 30[/"type:
node: NonEmptyString.Type"/] + 31[/"type:
node: NumberFromString.Type"/] + 32[/"type:
node: Circle.Type"/] + 33[/"type: null
node: null"/] + 34[/"type:
node: Circle.Encoded"/] + 35[/"type:
node: Square.Encoded"/] + 36[/"type: #123; Type: #quot;lookalike#quot;; #125;
node: #123; Type: #quot;lookalike#quot; as const #125;"/] + 37[/"type:
node: Lookalike.Type"/] + 38[/"type:
node: Circle.Type"/] + 39[/"type:
node: AnyReceiver.Type"/] + 40[/"type:
node: Circle.Type"/] + 41[/"type:
node: Circle.Type"/] + 42[/"type:
node: Square.Type"/] + 43[/"type:
node: Circle.Type"/] + 44[/"type:
node: Square.Type"/] + 45[/"type:
node: Circle.Type"/] + 46[/"type: true
node: true"/] + 47[/"type:
node: Narrowed.Type"/] + 48[/"type:
node: Circle.Type"/] + 49[/"type: Union#lt;readonly #91;Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;, Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;#93;#gt;
node: Schema.Union#40;#91;Circle, Square#93;#41;"/] + 50[/"type:
node: AlreadyComposed.Type"/] + 51[/"type: 42
node: 42"/] + 52[/"type:
node: Circle.Type"/] + 53[/"type:
node: Square.Type"/] + 54[/"type:
node: ForwardA.Type"/] + 55[/"type:
node: ForwardB.Type"/] + 56[/"type: #123; a: String; #125;
node: #123; a: Schema.String #125;"/] + 57["type: Struct#lt;#123; readonly a: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 58[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 59[/"type: #123; b: String; #125;
node: #123; b: Schema.String #125;"/] + 60["type: Struct#lt;#123; readonly b: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 61[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 62[/"type: String
node: Schema.String"/] + 63[/"type:
node: Models.SubItem.Type"/] + 64[/"type:
node: Circle.Type"/] + 65[/"type: #123; m: String; #125;
node: #123; m: Schema.String #125;"/] + 66["type: Struct#lt;#123; readonly m: String; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 67[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 68[/"type:
node: MutableSchema.Type"/] + 69[/"type:
node: Circle.Type"/] + 70[/"type:
node: Circle.Type"/] + 71[/"type:
node: Square.Type"/] + 72[/"type: #123; a: String; #125;
node: #123; a: S.String #125;"/] + 73["type: Struct#lt;#123; readonly a: String; #125;#gt;
callee: S.Struct
args: #91;#93;"] + 74[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: S.Struct"/] + 75[/"type: #123; b: Number; #125;
node: #123; b: S.Number #125;"/] + 76["type: Struct#lt;#123; readonly b: Number; #125;#gt;
callee: S.Struct
args: #91;#93;"] + 77[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: S.Struct"/] + 78[/"type:
node: A.Type"/] + 79[/"type:
node: B.Type"/] + 1 -->|"kind: pipe"| 2 + 3 -->|"kind: transformCallee"| 2 + 2 -->|"kind: usedBy"| 0 + 0 -->|"kind: pipe"| 4 + 5 -->|"kind: transformCallee"| 4 + 7 -->|"kind: pipe"| 8 + 9 -->|"kind: transformCallee"| 8 + 8 -->|"kind: usedBy"| 6 + 6 -->|"kind: pipe"| 10 + 11 -->|"kind: transformCallee"| 10 + 13 -->|"kind: pipe"| 14 + 15 -->|"kind: transformCallee"| 14 + 14 -->|"kind: usedBy"| 12 + 12 -->|"kind: pipe"| 16 + 17 -->|"kind: transformCallee"| 16 + 24 -->|"kind: pipe"| 25 + 26 -->|"kind: transformCallee"| 25 + 25 -->|"kind: usedBy"| 23 + 56 -->|"kind: pipe"| 57 + 58 -->|"kind: transformCallee"| 57 + 59 -->|"kind: pipe"| 60 + 61 -->|"kind: transformCallee"| 60 + 65 -->|"kind: pipe"| 66 + 67 -->|"kind: transformCallee"| 66 + 72 -->|"kind: pipe"| 73 + 74 -->|"kind: transformCallee"| 73 + 75 -->|"kind: pipe"| 76 + 77 -->|"kind: transformCallee"| 76 \ No newline at end of file diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt new file mode 100644 index 00000000..05b8fe3e --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.flows.txt @@ -0,0 +1 @@ +/.src/preferSchemaUnion.ts -> preferSchemaUnion.flows.preferSchemaUnion.mermaid diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt new file mode 100644 index 00000000..cfd7e2d8 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.layers.txt @@ -0,0 +1 @@ +==== /.src/preferSchemaUnion.ts (0 layer exports) ==== diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt new file mode 100644 index 00000000..c666d906 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.pipings.txt @@ -0,0 +1,183 @@ +==== /.src/preferSchemaUnion.ts (13 flows) ==== + +=== Piping Flow === +Location: 6:22 - 6:95 +Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("circle"), radius: Schema.Number } +Subject Type: { kind: Literal<"circle">; radius: Number; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }> + +=== Piping Flow === +Location: 6:44 - 6:69 +Node: Schema.Literal("circle") +Node Kind: KindCallExpression + +Subject: "circle" +Subject Type: "circle" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<"circle"> + +=== Piping Flow === +Location: 7:22 - 7:93 +Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("square"), side: Schema.Number } +Subject Type: { kind: Literal<"square">; side: Number; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly kind: Literal<"square">; readonly side: Number; }> + +=== Piping Flow === +Location: 7:44 - 7:69 +Node: Schema.Literal("square") +Node Kind: KindCallExpression + +Subject: "square" +Subject Type: "square" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<"square"> + +=== Piping Flow === +Location: 8:24 - 8:120 +Node: Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number } +Subject Type: { kind: Literal<"triangle">; base: Number; height: Number; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly kind: Literal<"triangle">; readonly base: Number; readonly height: Number; }> + +=== Piping Flow === +Location: 8:46 - 8:73 +Node: Schema.Literal("triangle") +Node Kind: KindCallExpression + +Subject: "triangle" +Subject Type: "triangle" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<"triangle"> + +=== Piping Flow === +Location: 14:31 - 17:3 +Node: Schema.Class("UserClass")({\n id: Schema.String,\n name: Schema.String\n}) +Node Kind: KindCallExpression + +Subject: {\n id: Schema.String,\n name: Schema.String\n} +Subject Type: { id: String; name: String; } + +Transformations (1): + [0] kind: call + callee: Schema.Class("UserClass") + args: (constant) + outType: Class, {}> + +=== Piping Flow === +Location: 39:31 - 39:62 +Node: Schema.Union([Circle, Square]) +Node Kind: KindCallExpression + +Subject: [Circle, Square] +Subject Type: [Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }>, Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>] + +Transformations (1): + [0] kind: call + callee: Schema.Union + args: (constant) + outType: Union; readonly radius: Number; }>, Struct<{ readonly kind: Literal<"square">; readonly side: Number; }>]> + +=== Piping Flow === +Location: 47:24 - 47:60 +Node: Schema.Struct({ a: Schema.String }) +Node Kind: KindCallExpression + +Subject: { a: Schema.String } +Subject Type: { a: String; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly a: String; }> + +=== Piping Flow === +Location: 48:24 - 48:60 +Node: Schema.Struct({ b: Schema.String }) +Node Kind: KindCallExpression + +Subject: { b: Schema.String } +Subject Type: { b: String; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly b: String; }> + +=== Piping Flow === +Location: 55:20 - 55:56 +Node: Schema.Struct({ m: Schema.String }) +Node Kind: KindCallExpression + +Subject: { m: Schema.String } +Subject Type: { m: String; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly m: String; }> + +=== Piping Flow === +Location: 63:17 - 63:43 +Node: S.Struct({ a: S.String }) +Node Kind: KindCallExpression + +Subject: { a: S.String } +Subject Type: { a: String; } + +Transformations (1): + [0] kind: call + callee: S.Struct + args: (constant) + outType: Struct<{ readonly a: String; }> + +=== Piping Flow === +Location: 64:17 - 64:43 +Node: S.Struct({ b: S.Number }) +Node Kind: KindCallExpression + +Subject: { b: S.Number } +Subject Type: { b: Number; } + +Transformations (1): + [0] kind: call + callee: S.Struct + args: (constant) + outType: Struct<{ readonly b: Number; }> diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt new file mode 100644 index 00000000..0323dc91 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion.quickfixes.txt @@ -0,0 +1,405 @@ +=== Quick Fix Inventory === + +[D1] (10:22-10:61) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + Fix 2: "Compose Schema.Union and derive type" + +[D2] (11:22-11:86) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + Fix 2: "Compose Schema.Union and derive type" + +[D3] (21:29-21:110) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + Fix 2: "Compose Schema.Union and derive type" + +[D4] (24:6-24:20) TS6196: 'MixedPrimitive' is declared but never used. + (no quick fixes) + +[D5] (25:6-25:18) TS6196: 'EncodedUnion' is declared but never used. + (no quick fixes) + +[D6] (27:6-27:20) TS6196: 'LookalikeUnion' is declared but never used. + (no quick fixes) + +[D7] (29:6-29:14) TS6196: 'AnyUnion' is declared but never used. + (no quick fixes) + +[D8] (30:6-30:18) TS6196: 'GenericAlias' is declared but never used. + (no quick fixes) + +[D9] (30:19-30:20) TS6196: 'T' is declared but never used. + (no quick fixes) + +[D10] (32:6-32:18) TS6196: 'SingleMember' is declared but never used. + (no quick fixes) + +[D11] (38:6-38:19) TS6196: 'NarrowedUnion' is declared but never used. + (no quick fixes) + +[D12] (44:30-44:69) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + +[D13] (46:28-46:71) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + +[D14] (53:31-53:78) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + +[D15] (56:28-56:74) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + +[D16] (60:3-60:42) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + +[D17] (65:28-65:57) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + Fix 2: "Compose Schema.Union and derive type" + +=== Quick Fix Application Results === + +=== [D1] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D1] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D1] Fix 2: "Compose Schema.Union and derive type" === + +--- file:///.src/preferSchemaUnion.ts --- +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema, Schema as S } from "effect" + +// 1. Two and three concrete schema leaves, including parenthesized unions +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + +export const Shape2 = Schema.Union([Circle, Square]); +export type Shape2 = typeof Shape2.Type +export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type + +// 2. Schema.Class, refinements and transformed codecs +export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String +}) {} +export const NonEmptyString = Schema.NonEmptyString +export const NumberFromString = Schema.NumberFromString + +export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type + +// 3. Negative cases - NO DIAGNOSTIC +type MixedPrimitive = typeof Circle.Type | null +type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded +const Lookalike = { Type: "lookalike" as const } +type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type +declare const AnyReceiver: any +type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type +type GenericAlias = typeof Circle.Type | typeof Square.Type +declare type AmbientAlias = typeof Circle.Type | typeof Square.Type +type SingleMember = typeof Circle.Type +type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string +} +declare const Narrowed: NarrowedSchemaWithoutEncoded +type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type +export const AlreadyComposed = Schema.Union([Circle, Square]) +export type AlreadyComposed = typeof AlreadyComposed.Type + +// 4. Diagnosed, but safety checks withhold code action +export const CollidingValue = 42 +export type CollidingValue = typeof Circle.Type | typeof Square.Type + +export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type +export const ForwardA = Schema.Struct({ a: Schema.String }) +export const ForwardB = Schema.Struct({ b: Schema.String }) + +namespace Models { + export const SubItem = Schema.String +} +export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + +let MutableSchema = Schema.Struct({ m: Schema.String }) +export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + +export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + +// 5. Existing aliased Schema import +export const A = S.Struct({ a: S.String }) +export const B = S.Struct({ b: S.Number }) +export type AliasedUnion = typeof A.Type | typeof B.Type + + +=== [D2] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D2] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D2] Fix 2: "Compose Schema.Union and derive type" === + +--- file:///.src/preferSchemaUnion.ts --- +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema, Schema as S } from "effect" + +// 1. Two and three concrete schema leaves, including parenthesized unions +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + +export type Shape2 = typeof Circle.Type | typeof Square.Type +export const Shape3 = Schema.Union([Circle, Square, Triangle]); +export type Shape3 = typeof Shape3.Type + +// 2. Schema.Class, refinements and transformed codecs +export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String +}) {} +export const NonEmptyString = Schema.NonEmptyString +export const NumberFromString = Schema.NumberFromString + +export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type + +// 3. Negative cases - NO DIAGNOSTIC +type MixedPrimitive = typeof Circle.Type | null +type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded +const Lookalike = { Type: "lookalike" as const } +type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type +declare const AnyReceiver: any +type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type +type GenericAlias = typeof Circle.Type | typeof Square.Type +declare type AmbientAlias = typeof Circle.Type | typeof Square.Type +type SingleMember = typeof Circle.Type +type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string +} +declare const Narrowed: NarrowedSchemaWithoutEncoded +type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type +export const AlreadyComposed = Schema.Union([Circle, Square]) +export type AlreadyComposed = typeof AlreadyComposed.Type + +// 4. Diagnosed, but safety checks withhold code action +export const CollidingValue = 42 +export type CollidingValue = typeof Circle.Type | typeof Square.Type + +export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type +export const ForwardA = Schema.Struct({ a: Schema.String }) +export const ForwardB = Schema.Struct({ b: Schema.String }) + +namespace Models { + export const SubItem = Schema.String +} +export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + +let MutableSchema = Schema.Struct({ m: Schema.String }) +export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + +export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + +// 5. Existing aliased Schema import +export const A = S.Struct({ a: S.String }) +export const B = S.Struct({ b: S.Number }) +export type AliasedUnion = typeof A.Type | typeof B.Type + + +=== [D3] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D3] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D3] Fix 2: "Compose Schema.Union and derive type" === + +--- file:///.src/preferSchemaUnion.ts --- +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema, Schema as S } from "effect" + +// 1. Two and three concrete schema leaves, including parenthesized unions +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + +export type Shape2 = typeof Circle.Type | typeof Square.Type +export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type + +// 2. Schema.Class, refinements and transformed codecs +export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String +}) {} +export const NonEmptyString = Schema.NonEmptyString +export const NumberFromString = Schema.NumberFromString + +export const ValidEntities = Schema.Union([UserClass, NonEmptyString, NumberFromString]); +export type ValidEntities = typeof ValidEntities.Type + +// 3. Negative cases - NO DIAGNOSTIC +type MixedPrimitive = typeof Circle.Type | null +type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded +const Lookalike = { Type: "lookalike" as const } +type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type +declare const AnyReceiver: any +type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type +type GenericAlias = typeof Circle.Type | typeof Square.Type +declare type AmbientAlias = typeof Circle.Type | typeof Square.Type +type SingleMember = typeof Circle.Type +type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string +} +declare const Narrowed: NarrowedSchemaWithoutEncoded +type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type +export const AlreadyComposed = Schema.Union([Circle, Square]) +export type AlreadyComposed = typeof AlreadyComposed.Type + +// 4. Diagnosed, but safety checks withhold code action +export const CollidingValue = 42 +export type CollidingValue = typeof Circle.Type | typeof Square.Type + +export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type +export const ForwardA = Schema.Struct({ a: Schema.String }) +export const ForwardB = Schema.Struct({ b: Schema.String }) + +namespace Models { + export const SubItem = Schema.String +} +export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + +let MutableSchema = Schema.Struct({ m: Schema.String }) +export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + +export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + +// 5. Existing aliased Schema import +export const A = S.Struct({ a: S.String }) +export const B = S.Struct({ b: S.Number }) +export type AliasedUnion = typeof A.Type | typeof B.Type + + +=== [D12] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D12] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D13] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D13] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D14] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D14] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D15] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D15] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D16] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D16] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D17] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D17] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D17] Fix 2: "Compose Schema.Union and derive type" === + +--- file:///.src/preferSchemaUnion.ts --- +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema, Schema as S } from "effect" + +// 1. Two and three concrete schema leaves, including parenthesized unions +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + +export type Shape2 = typeof Circle.Type | typeof Square.Type +export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type + +// 2. Schema.Class, refinements and transformed codecs +export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String +}) {} +export const NonEmptyString = Schema.NonEmptyString +export const NumberFromString = Schema.NumberFromString + +export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type + +// 3. Negative cases - NO DIAGNOSTIC +type MixedPrimitive = typeof Circle.Type | null +type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded +const Lookalike = { Type: "lookalike" as const } +type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type +declare const AnyReceiver: any +type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type +type GenericAlias = typeof Circle.Type | typeof Square.Type +declare type AmbientAlias = typeof Circle.Type | typeof Square.Type +type SingleMember = typeof Circle.Type +type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string +} +declare const Narrowed: NarrowedSchemaWithoutEncoded +type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type +export const AlreadyComposed = Schema.Union([Circle, Square]) +export type AlreadyComposed = typeof AlreadyComposed.Type + +// 4. Diagnosed, but safety checks withhold code action +export const CollidingValue = 42 +export type CollidingValue = typeof Circle.Type | typeof Square.Type + +export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type +export const ForwardA = Schema.Struct({ a: Schema.String }) +export const ForwardB = Schema.Struct({ b: Schema.String }) + +namespace Models { + export const SubItem = Schema.String +} +export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + +let MutableSchema = Schema.Struct({ m: Schema.String }) +export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + +export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + +// 5. Existing aliased Schema import +export const A = S.Struct({ a: S.String }) +export const B = S.Struct({ b: S.Number }) +export const AliasedUnion = Schema.Union([A, B]); +export type AliasedUnion = typeof AliasedUnion.Type + diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt new file mode 100644 index 00000000..8c303570 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.errors.txt @@ -0,0 +1,18 @@ +=== Metadata === +Effect version: 4.0.0 + +/.src/preferSchemaUnion_preview.ts(8,21): warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + + +==== /.src/preferSchemaUnion_preview.ts (1 errors) ==== + // @effect-diagnostics *:off + // @effect-diagnostics preferSchemaUnion:warning + import { Schema } from "effect" + + export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) + export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + + export type Shape = typeof Circle.Type | typeof Square.Type + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +!!! warning TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid new file mode 100644 index 00000000..0006ccc0 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid @@ -0,0 +1,25 @@ +flowchart TB + 0[/"type: #123; kind: Literal#lt;#quot;circle#quot;#gt;; radius: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;circle#quot;#41;, radius: Schema.Number #125;"/] + 1[/"type: #quot;circle#quot;
node: #quot;circle#quot;"/] + 2["type: Literal#lt;#quot;circle#quot;#gt;
callee: Schema.Literal
args: #91;#93;"] + 3[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/] + 4["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;circle#quot;#gt;; readonly radius: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 5[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 6[/"type: #123; kind: Literal#lt;#quot;square#quot;#gt;; side: Number; #125;
node: #123; kind: Schema.Literal#40;#quot;square#quot;#41;, side: Schema.Number #125;"/] + 7[/"type: #quot;square#quot;
node: #quot;square#quot;"/] + 8["type: Literal#lt;#quot;square#quot;#gt;
callee: Schema.Literal
args: #91;#93;"] + 9[/"type: #lt;L extends SchemaAST.LiteralValue#gt;#40;literal: L#41; =#gt; Literal#lt;L#gt;
node: Schema.Literal"/] + 10["type: Struct#lt;#123; readonly kind: Literal#lt;#quot;square#quot;#gt;; readonly side: Number; #125;#gt;
callee: Schema.Struct
args: #91;#93;"] + 11[/"type: #lt;const Fields extends Struct.Fields#gt;#40;fields: Fields#41; =#gt; Struct#lt;Fields#gt;
node: Schema.Struct"/] + 12[/"type:
node: Circle.Type"/] + 13[/"type:
node: Square.Type"/] + 1 -->|"kind: pipe"| 2 + 3 -->|"kind: transformCallee"| 2 + 2 -->|"kind: usedBy"| 0 + 0 -->|"kind: pipe"| 4 + 5 -->|"kind: transformCallee"| 4 + 7 -->|"kind: pipe"| 8 + 9 -->|"kind: transformCallee"| 8 + 8 -->|"kind: usedBy"| 6 + 6 -->|"kind: pipe"| 10 + 11 -->|"kind: transformCallee"| 10 \ No newline at end of file diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt new file mode 100644 index 00000000..0637ed6d --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.flows.txt @@ -0,0 +1 @@ +/.src/preferSchemaUnion_preview.ts -> preferSchemaUnion_preview.flows.preferSchemaUnion_preview.mermaid diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt new file mode 100644 index 00000000..33b2870e --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.layers.txt @@ -0,0 +1 @@ +==== /.src/preferSchemaUnion_preview.ts (0 layer exports) ==== diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt new file mode 100644 index 00000000..9e0b705c --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.pipings.txt @@ -0,0 +1,57 @@ +==== /.src/preferSchemaUnion_preview.ts (4 flows) ==== + +=== Piping Flow === +Location: 5:22 - 5:95 +Node: Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("circle"), radius: Schema.Number } +Subject Type: { kind: Literal<"circle">; radius: Number; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly kind: Literal<"circle">; readonly radius: Number; }> + +=== Piping Flow === +Location: 5:44 - 5:69 +Node: Schema.Literal("circle") +Node Kind: KindCallExpression + +Subject: "circle" +Subject Type: "circle" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<"circle"> + +=== Piping Flow === +Location: 6:22 - 6:93 +Node: Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +Node Kind: KindCallExpression + +Subject: { kind: Schema.Literal("square"), side: Schema.Number } +Subject Type: { kind: Literal<"square">; side: Number; } + +Transformations (1): + [0] kind: call + callee: Schema.Struct + args: (constant) + outType: Struct<{ readonly kind: Literal<"square">; readonly side: Number; }> + +=== Piping Flow === +Location: 6:44 - 6:69 +Node: Schema.Literal("square") +Node Kind: KindCallExpression + +Subject: "square" +Subject Type: "square" + +Transformations (1): + [0] kind: call + callee: Schema.Literal + args: (constant) + outType: Literal<"square"> diff --git a/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt new file mode 100644 index 00000000..6221cf00 --- /dev/null +++ b/testdata/baselines/reference/effect-v4/preferSchemaUnion_preview.quickfixes.txt @@ -0,0 +1,28 @@ +=== Quick Fix Inventory === + +[D1] (8:21-8:60) TS377130: This type alias unions decoded Effect Schema types. Prefer Schema.Union([...]) and derive the type from the resulting schema. effect(preferSchemaUnion) + Fix 0: "Disable preferSchemaUnion for this line" + Fix 1: "Disable preferSchemaUnion for entire file" + Fix 2: "Compose Schema.Union and derive type" + +=== Quick Fix Application Results === + +=== [D1] Fix 0: "Disable preferSchemaUnion for this line" === +skipped by default + +=== [D1] Fix 1: "Disable preferSchemaUnion for entire file" === +skipped by default + +=== [D1] Fix 2: "Compose Schema.Union and derive type" === + +--- file:///.src/preferSchemaUnion_preview.ts --- +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema } from "effect" + +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export const Shape = Schema.Union([Circle, Square]); +export type Shape = typeof Shape.Type + diff --git a/testdata/tests/effect-v3/preferSchemaUnion.ts b/testdata/tests/effect-v3/preferSchemaUnion.ts new file mode 100644 index 00000000..e06162e4 --- /dev/null +++ b/testdata/tests/effect-v3/preferSchemaUnion.ts @@ -0,0 +1,9 @@ +// @effect-v3 +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema } from "effect" + +const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export type Shape = typeof Circle.Type | typeof Square.Type diff --git a/testdata/tests/effect-v4/preferSchemaUnion.ts b/testdata/tests/effect-v4/preferSchemaUnion.ts new file mode 100644 index 00000000..cc57878a --- /dev/null +++ b/testdata/tests/effect-v4/preferSchemaUnion.ts @@ -0,0 +1,65 @@ +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema, Schema as S } from "effect" + +// 1. Two and three concrete schema leaves, including parenthesized unions +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) +export const Triangle = Schema.Struct({ kind: Schema.Literal("triangle"), base: Schema.Number, height: Schema.Number }) + +export type Shape2 = typeof Circle.Type | typeof Square.Type +export type Shape3 = (typeof Circle.Type | typeof Square.Type) | typeof Triangle.Type + +// 2. Schema.Class, refinements and transformed codecs +export class UserClass extends Schema.Class("UserClass")({ + id: Schema.String, + name: Schema.String +}) {} +export const NonEmptyString = Schema.NonEmptyString +export const NumberFromString = Schema.NumberFromString + +export type ValidEntities = typeof UserClass.Type | typeof NonEmptyString.Type | typeof NumberFromString.Type + +// 3. Negative cases - NO DIAGNOSTIC +type MixedPrimitive = typeof Circle.Type | null +type EncodedUnion = typeof Circle.Encoded | typeof Square.Encoded +const Lookalike = { Type: "lookalike" as const } +type LookalikeUnion = typeof Lookalike.Type | typeof Circle.Type +declare const AnyReceiver: any +type AnyUnion = typeof AnyReceiver.Type | typeof Circle.Type +type GenericAlias = typeof Circle.Type | typeof Square.Type +declare type AmbientAlias = typeof Circle.Type | typeof Square.Type +type SingleMember = typeof Circle.Type +type NarrowedSchemaWithoutEncoded = { + readonly "~effect/Schema/Schema": true + readonly Type: string +} +declare const Narrowed: NarrowedSchemaWithoutEncoded +type NarrowedUnion = typeof Narrowed.Type | typeof Circle.Type +export const AlreadyComposed = Schema.Union([Circle, Square]) +export type AlreadyComposed = typeof AlreadyComposed.Type + +// 4. Diagnosed, but safety checks withhold code action +export const CollidingValue = 42 +export type CollidingValue = typeof Circle.Type | typeof Square.Type + +export type ForwardUnion = typeof ForwardA.Type | typeof ForwardB.Type +export const ForwardA = Schema.Struct({ a: Schema.String }) +export const ForwardB = Schema.Struct({ b: Schema.String }) + +namespace Models { + export const SubItem = Schema.String +} +export type NamespacedUnion = typeof Models.SubItem.Type | typeof Circle.Type + +let MutableSchema = Schema.Struct({ m: Schema.String }) +export type MutableUnion = typeof MutableSchema.Type | typeof Circle.Type + +export type CommentedUnion = + // interior comment + typeof Circle.Type | typeof Square.Type + +// 5. Existing aliased Schema import +export const A = S.Struct({ a: S.String }) +export const B = S.Struct({ b: S.Number }) +export type AliasedUnion = typeof A.Type | typeof B.Type diff --git a/testdata/tests/effect-v4/preferSchemaUnion_preview.ts b/testdata/tests/effect-v4/preferSchemaUnion_preview.ts new file mode 100644 index 00000000..58ed4d60 --- /dev/null +++ b/testdata/tests/effect-v4/preferSchemaUnion_preview.ts @@ -0,0 +1,8 @@ +// @effect-diagnostics *:off +// @effect-diagnostics preferSchemaUnion:warning +import { Schema } from "effect" + +export const Circle = Schema.Struct({ kind: Schema.Literal("circle"), radius: Schema.Number }) +export const Square = Schema.Struct({ kind: Schema.Literal("square"), side: Schema.Number }) + +export type Shape = typeof Circle.Type | typeof Square.Type