Something went wrong. Try again.
A Thrift language server, formatter, and linter.
lsp linter helix vim vscode thrift
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419package analyzers
import ( "context" "fmt" "slices" "strings"
"github.com/karitham/thrift-ls/sema" "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax")
type SemanticAnalysis struct{}
func (s *SemanticAnalysis) Name() string { return "SemanticAnalysis"}
func (s *SemanticAnalysis) AnalyzeFile(ctx context.Context, f sema.File) ([]sema.Diagnostic, error) { // The run's shared index: resolutions are memoized per (file, name), // across every analyzer in the run. return s.checkDefinitionExist(ctx, f.View(), f.Index(), f.PF), nil}
// checkDefinitionExist reports undefined references in one document walk:// type references that resolve to no definition (fields, signatures,// consts, typedefs, nested container elements), constant-value identifiers// resolving to no enum value or const at any nesting depth, and field defaults// or const values whose kind — at any literal depth — mismatches the declared// type's underlying kind.func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, view sema.Graph, ix *sema.Index, pf *store.ParsedFile) []sema.Diagnostic { res := make([]sema.Diagnostic, 0)
syntax.Walk(pf.AST(), func(n syntax.Node) bool { switch v := n.(type) { case *syntax.FieldType: res = append(res, s.checkTypeExist(ctx, view, ix, pf, v)...) case *syntax.Field: if v.Value != nil { res = append(res, s.checkValueType(ctx, ix, pf, pf, v.Type, v.Value)...) } case *syntax.Const: res = append(res, s.checkValueType(ctx, ix, pf, pf, v.Type, v.Value)...) case *syntax.ConstValue: res = append(res, s.checkConstValueExist(ctx, view, ix, pf, v)...) case *syntax.StructuredAnnotation: res = append(res, s.checkAnnotationTypeExist(ctx, view, ix, pf, v)...) }
return true })
return res}
// checkAnnotationTypeExist reports a structured annotation whose name// resolves to no definition. Matching the upfluence compiler, the name of// a structured annotation must be a declared type: the compiler errors at// parse time ("Type %s does not exist"); here the semantic pass owns it.func (s *SemanticAnalysis) checkAnnotationTypeExist(ctx context.Context, view sema.Graph, ix *sema.Index, pf *store.ParsedFile, sa *syntax.StructuredAnnotation,) (res []sema.Diagnostic) { if sa == nil || sa.Name == nil { return res }
ft := &syntax.FieldType{Kind: syntax.TypeIdent, Ident: sa.Name}
def, err := ix.ResolveType(ctx, pf, ft) if err == nil && def != nil { return res }
return append(res, sema.Diagnostic{ Span: sema.SpanOf(pf, sa.Name), Severity: sema.SeverityError, Code: sema.CodeUnknownAnnotation, Message: "annotation type doesn't exist", })}
func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, view sema.Graph, ix *sema.Index, pf *store.ParsedFile, cst *syntax.ConstValue,) (res []sema.Diagnostic) { if cst == nil || cst.Kind != syntax.ValueIdent { return res }
if cst.Text == "true" || cst.Text == "false" { return res }
def, err := ix.ResolveValue(ctx, pf, cst) if err != nil || def == nil { res = append(res, sema.Diagnostic{ Span: sema.SpanOf(pf, cst), Severity: sema.SeverityError, Code: sema.CodeUndefinedValue, Message: "default value doesn't exist", }) }
return res}
// checkValueType reports a field default or const value whose literal// contradicts the declared type's underlying kind: typedef chains are// followed (across includes) before comparing, so "typedef map<string,// string> M" accepts map literals, and the comparison recurses — container// entries and struct field values are each classified against their own// resolved type. reportPf backs the literal's ranges; scopePf is the file// whose scope expect resolves in — the two part ways once the type walks// into an include. Existence of identifier values is checkConstValueExist's// job; this only classifies kinds.func (s *SemanticAnalysis) checkValueType(ctx context.Context, ix *sema.Index, reportPf, scopePf *store.ParsedFile, expect *syntax.FieldType, value *syntax.ConstValue) []sema.Diagnostic { if value == nil { return nil }
ut, typePf := ix.UnderlyingType(ctx, scopePf, expect) if ut == nil { // The declared type doesn't resolve; checkTypeExist already // reports it, and no kind comparison is meaningful. return nil }
switch value.Kind { case syntax.ValueMap: if ut.Kind == syntax.TypeMap { return s.checkMapEntries(ctx, ix, reportPf, typePf, ut, value) }
if fields, structPf := structFields(ctx, ix, ut, typePf); fields != nil { return s.checkStructEntries(ctx, ix, reportPf, underlyingName(ut), fields, structPf, value) }
case syntax.ValueList: if isKind(ut, syntax.TypeList, syntax.TypeSet) { return s.checkListEntries(ctx, ix, reportPf, typePf, ut, value) }
case syntax.ValueString: if isBase(ut, syntax.TokenString) { return nil }
case syntax.ValueDouble: if isBase(ut, syntax.TokenDouble) { return nil }
case syntax.ValueInt: if intValueMatches(ctx, ix, ut, typePf, value) { return nil }
case syntax.ValueIdent: if identValueMatches(ctx, ix, reportPf, ut, value) { return nil } }
return kindMismatch(reportPf, value, ut)}
// checkMapEntries validates each entry of a map literal against the map// type's resolved key and value types.func (s *SemanticAnalysis) checkMapEntries(ctx context.Context, ix *sema.Index, reportPf, scopePf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []sema.Diagnostic { var res []sema.Diagnostic
for _, entry := range value.Map { res = append(res, s.checkValueType(ctx, ix, reportPf, scopePf, ut.KeyType, entry.Key)...) res = append(res, s.checkValueType(ctx, ix, reportPf, scopePf, ut.ValueType, entry.Value)...) }
return res}
// checkListEntries validates each element of a list literal against the// container's resolved element type.func (s *SemanticAnalysis) checkListEntries(ctx context.Context, ix *sema.Index, reportPf, scopePf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []sema.Diagnostic { var res []sema.Diagnostic
for _, elem := range value.List { res = append(res, s.checkValueType(ctx, ix, reportPf, scopePf, ut.ValueType, elem)...) }
return res}
// checkStructEntries validates a struct-valued map literal: keys must name// fields of the resolved definition, and each value is classified against// the field's own type.func (s *SemanticAnalysis) checkStructEntries(ctx context.Context, ix *sema.Index, reportPf *store.ParsedFile, typeName string, fields []*syntax.Field, structPf *store.ParsedFile, value *syntax.ConstValue) []sema.Diagnostic { byName := make(map[string]*syntax.Field, len(fields)) for _, f := range fields { if f.Name != nil { byName[f.Name.Text] = f } }
var res []sema.Diagnostic
for _, entry := range value.Map { if entry.Key.Kind != syntax.ValueString { res = append(res, *mismatchDiagnostic(reportPf, entry.Key, "field name", kindName(entry.Key.Kind))) continue }
name := strings.Trim(entry.Key.Text, "'\"")
f, ok := byName[name] if !ok { res = append(res, unknownFieldDiagnostic(reportPf, entry.Key, typeName, name)) continue }
res = append(res, s.checkValueType(ctx, ix, reportPf, structPf, f.Type, entry.Value)...) }
return res}
// kindMismatch reports a literal whose kind contradicts the target type.func kindMismatch(pf *store.ParsedFile, value *syntax.ConstValue, ut *syntax.FieldType) []sema.Diagnostic { return []sema.Diagnostic{*mismatchDiagnostic(pf, value, underlyingName(ut), gotName(value))}}
// gotName names a literal's kind for a mismatch message. true/false lex as// int constants but are bools.func gotName(value *syntax.ConstValue) string { if value.Kind == syntax.ValueInt && (value.Text == "true" || value.Text == "false") { return "bool" }
return kindName(value.Kind)}
// unknownFieldDiagnostic reports a struct-valued literal keyed by a name// that is not a field of the definition. name is the key text with its// quotes stripped.func unknownFieldDiagnostic(pf *store.ParsedFile, key *syntax.ConstValue, typeName, name string) sema.Diagnostic { return sema.Diagnostic{ Span: sema.SpanOf(pf, key), Severity: sema.SeverityError, Code: sema.CodeValueTypeMismatch, Message: fmt.Sprintf("no field named %q in %s", name, typeName), }}
// isKind reports whether the underlying type is one of the container kinds.func isKind(ft *syntax.FieldType, kinds ...syntax.FieldTypeKind) bool { if ft == nil { return false }
return slices.Contains(kinds, ft.Kind)}
// isBase reports whether the underlying type is the given base keyword.func isBase(ft *syntax.FieldType, base syntax.TokenKind) bool { return ft != nil && ft.Kind == syntax.TypeBase && ft.Base == base}
// structFields resolves the underlying type to a struct, union, or// exception and returns its fields with the file whose scope the fields'// types resolve in — the compiler accepts map literals, keyed by field// name, for struct-valued constants and field defaults. nil fields when// the type is not a struct-like definition.func structFields(ctx context.Context, ix *sema.Index, ut *syntax.FieldType, typePf *store.ParsedFile) ([]*syntax.Field, *store.ParsedFile) { if ut == nil || ut.Kind != syntax.TypeIdent { return nil, nil }
def, err := ix.ResolveType(ctx, typePf, ut) if err != nil || def == nil || def.Parsed == nil { return nil, nil }
s, ok := def.Node.(*syntax.Struct) if !ok { return nil, nil }
return s.Fields, def.Parsed}
// identValueMatches reports whether an identifier literal is acceptable in// the target position. Any target takes an identifier: it may name a const// or enum value of matching type, and existence is checkConstValueExist's// job — an identifier resolving to nothing stays silent here. A bool// target accepts only a reference to a const whose declared type is bool —// the compiler substitutes bool-const identifiers before validating — so// the reference must resolve, in the referencing file's scope, to a const// of bool type.func identValueMatches(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) bool { if !isBase(ut, syntax.TokenBool) { return true }
def, err := ix.ResolveValue(ctx, pf, value) if err != nil || def == nil { return true }
c, ok := def.Node.(*syntax.Const) if !ok { // An enum value reference is not a bool. return false }
bt, _ := ix.UnderlyingType(ctx, def.Parsed, c.Type)
return isBase(bt, syntax.TokenBool)}
// intValueMatches reports whether an integer literal fits the underlying// type: an integer base, or an enum — the compiler accepts int constants// in enum positions.func intValueMatches(ctx context.Context, ix *sema.Index, ut *syntax.FieldType, typePf *store.ParsedFile, value *syntax.ConstValue) bool { if value.Text == "true" || value.Text == "false" { return isBase(ut, syntax.TokenBool) }
if ut == nil { return false }
switch ut.Kind { case syntax.TypeBase: switch ut.Base { case syntax.TokenByte, syntax.TokenI8, syntax.TokenI16, syntax.TokenI32, syntax.TokenI64: return true } case syntax.TypeIdent: def, err := ix.ResolveType(ctx, typePf, ut) return err == nil && def != nil && def.Kind == sema.DefinitionEnum }
return false}
// underlyingName renders the underlying type for a mismatch message: the// resolved container or base kind, or the unresolved identifier as written.func underlyingName(ft *syntax.FieldType) string { if ft == nil { return "unknown" }
switch ft.Kind { case syntax.TypeIdent: if ft.Ident != nil { return ft.Ident.Text } case syntax.TypeMap: return "map" case syntax.TypeList: return "list" case syntax.TypeSet: return "set" case syntax.TypeBase: return ft.Base.String() }
return "unknown"}
func kindName(kind syntax.ConstValueKind) string { switch kind { case syntax.ValueList: return "list" case syntax.ValueMap: return "map" case syntax.ValueString: return "string" case syntax.ValueDouble: return "double" case syntax.ValueInt: return "int" case syntax.ValueIdent: return "identifier" }
return "unknown"}
func mismatchDiagnostic(pf *store.ParsedFile, value *syntax.ConstValue, expect, got string) *sema.Diagnostic { return &sema.Diagnostic{ Span: sema.SpanOf(pf, value), Severity: sema.SeverityError, Code: sema.CodeValueTypeMismatch, Message: fmt.Sprintf("expect %s but got %s", expect, got), }}
// checkTypeExist reports a single type reference that resolves to no// definition. The walk visits every FieldType individually — including// container element types — so this needs no recursion.func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, view sema.Graph, ix *sema.Index, pf *store.ParsedFile, ft *syntax.FieldType,) (res []sema.Diagnostic) { if ft == nil || ft.Kind != syntax.TypeIdent { return res }
def, err := ix.ResolveType(ctx, pf, ft) if err == nil && def != nil { return res }
return append(res, sema.Diagnostic{ Span: sema.SpanOf(pf, ft.Ident), Severity: sema.SeverityError, Code: sema.CodeUndefinedType, Message: "field type doesn't exist", })}