sema

package
v0.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package sema owns thrift-ls's semantic analysis: the lint pipeline, the diagnostics it produces, and the fixes attached to them. All positions are parser coordinates; frontends translate to their own representations.

Index

Constants

View Source
const (
	CodeParseError        = "parse-error"
	CodeIncludeCycle      = "include-cycle"
	CodeFieldIDRange      = "field-id-range"
	CodeFieldIDConflict   = "field-id-conflict"
	CodeDuplicateDef      = "duplicate-definition"
	CodeDuplicateEnumVal  = "duplicate-enum-value"
	CodeDuplicateValue    = "duplicate-value"
	CodeImplicitEnumValue = "implicit-enum-value"
	CodeUnusedInclude     = "unused-include"
	CodeIncludeShadow     = "include-shadow"
	CodeUndefinedType     = "undefined-type"
	CodeUndefinedValue    = "undefined-value"
	CodeValueTypeMismatch = "value-type-mismatch"
	CodeNonScalarMapKey   = "non-scalar-map-key"
	CodeUnknownAnnotation = "unknown-annotation-type"
)

Diagnostic codes carried on every diagnostic the pipeline reports. Fix providers and frontend filters match on these — never on the message text, which is free to change.

View Source
const NonScalarMapKeyCheckName = "NonScalarMapKeyCheck"

NonScalarMapKeyCheckName is the configuration name of NonScalarMapKeyCheck.

Variables

This section is empty.

Functions

func BareName

func BareName(name string) string

BareName strips the include qualifier from a name: "base.User" becomes "User". References in files that include the definition file use the bare name, so qualified literals must match against it too.

func IncludeNameOf

func IncludeNameOf(file uri.URI) string

IncludeNameOf returns the include name of a file URI: the base name without extension. file:///base.thrift -> "base".

func IsBasicType

func IsBasicType(t string) bool

IsBasicType reports whether t is a built-in base type.

func ParseIdent

func ParseIdent(cur uri.URI, includes []*syntax.Include, identifier string) (include, ident string)

ParseIdent parses an identifier. identifier format:

  1. identifier
  2. include.identifier

it returns include, ident

func RefKindsFor

func RefKindsFor(k DefinitionKind) []store.RefKind

RefKindsFor returns the reference slots a definition kind can appear in.

An exception is thrown (signatures) but never used as a field type; as an annotation type it is legal, since the compiler's get_type resolves any declared type. Enum values and consts live in value positions. Services are extends-only. Every other type can appear in field, signature, and annotation-type slots.

func TypeReferenceName

func TypeReferenceName(ft *syntax.FieldType) string

TypeReferenceName returns the referenced type name of a FieldType, or "" for base types and containers.

Types

type Action

type Action struct {
	Title string
	Fix   bool // true: quickfix for a diagnostic; false: refactor
	File  uri.URI
	Edits []Edit
}

Action is an offered source edit: a quickfix for a diagnostic or a refactor.

type ActionProvider

type ActionProvider interface {
	Actions(ctx context.Context, f File, span Span, report Report) []Action
}

ActionProvider offers source edits for a selection, independent of any diagnostic: refactors. The report is available for providers whose actions double as quickfixes for diagnostics overlapping the selection.

type AddIncludeFixer

type AddIncludeFixer struct{}

AddIncludeFixer offers the include that makes an undefined type resolve: the definition is searched across the workspace, so the fix works project-wide. It fixes only "undefined-type" diagnostics.

func (AddIncludeFixer) Fix

func (f AddIncludeFixer) Fix(ctx context.Context, file File, d Diagnostic) []Fix

type Analyzer

type Analyzer interface {
	Name() string
	Analyze(ctx context.Context, run *Run) error
}

Analyzer is a whole-run check: its findings may span files (include cycles are the current consumer), and it may need to see files that do not parse (Parse is the current consumer).

func Defaults

func Defaults() []Analyzer

Defaults returns the built-in analyzers.

func EachFile

func EachFile(a FileAnalyzer) Analyzer

EachFile adapts a FileAnalyzer to Analyzer.

type Config

type Config struct {
	// Disabled names analyzers (by Name) to skip. nil runs all.
	Disabled []string
	// Severity overrides a diagnostic's severity by code.
	Severity map[string]Severity
}

Config selects and tunes analyzers.

func ConfigFromLint

func ConfigFromLint(disabled []string, severity map[string]string) Config

ConfigFromLint builds a Config from the config layer's lint settings: the analyzer names to skip and the severity overrides by code, with severities named "error", "warning", "info", and "hint". Unknown names are ignored; the config sources validate them.

type CycleCheck

type CycleCheck struct{}

CycleCheck reports include edges that close a cycle: the include X -> Y is reported when Y transitively includes X back. Cycles of any length are caught, including self-includes.

func (*CycleCheck) Analyze

func (c *CycleCheck) Analyze(ctx context.Context, run *Run) error

func (*CycleCheck) Name

func (c *CycleCheck) Name() string

type DefinitionKind

type DefinitionKind uint8

DefinitionKind identifies the kind of a resolved definition.

const (
	DefinitionNone DefinitionKind = iota
	DefinitionStruct
	DefinitionUnion
	DefinitionException
	DefinitionEnum
	DefinitionTypedef
	DefinitionConst
	DefinitionEnumValue
	DefinitionService
)

type Diagnostic

type Diagnostic struct {
	Code     string // stable identity; never matched by message
	Severity Severity
	Message  string
	Span     Span
	Fixes    []Fix
}

Diagnostic is one finding. Fixes are edits that resolve it, computed by the analyzer that reported the diagnostic.

type DuplicateCheck

type DuplicateCheck struct{}

DuplicateCheck reports the duplicates the compiler rejects: a name defined twice in one scope — structs, unions, exceptions, enums, members, fields, function arguments, service functions, typedefs, constants — enum members whose resolved value collides, and map keys or set values that repeat in a constant. Duplicates are ambiguous on the wire and in generated code, so they are errors.

func (*DuplicateCheck) AnalyzeFile

func (c *DuplicateCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*DuplicateCheck) Name

func (c *DuplicateCheck) Name() string

type Edit

type Edit struct {
	Span    Span
	NewText string
}

Edit replaces Span with NewText in the file the diagnostic belongs to. An empty Span inserts.

type EnumImplicitValue

type EnumImplicitValue struct {
	Member *syntax.EnumValue
	Value  int64
	Known  bool // false when the preceding value is broken, so Value is unknowable
}

EnumImplicitValue is an enum member that lacks an explicit value, with the int constant the compiler auto-increments for it.

func EnumImplicitValues

func EnumImplicitValues(enum *syntax.Enum) []EnumImplicitValue

EnumImplicitValues reports the members of an enum that carry no explicit value, together with the value the compiler would auto-increment: 0 for the first member, one greater than the preceding member's value otherwise. Members after an unparseable explicit constant report Known=false until the next parseable constant settles the chain.

type EnumValueCheck

type EnumValueCheck struct{}

func (*EnumValueCheck) AnalyzeFile

func (c *EnumValueCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*EnumValueCheck) Name

func (c *EnumValueCheck) Name() string

EnumValueCheck warns on enum members that lack an explicit value.

The compiler auto-increments implicit members: 0 for the first member, one greater than the preceding member's value otherwise. Their on-wire value therefore follows their position; inserting, removing, or reordering members silently changes serialized data.

type EnumValuesProvider

type EnumValuesProvider struct{}

EnumValuesProvider offers the rewrite that appends an explicit value to every member of the enum containing the selection, mirroring the auto-incremented constants the compiler would assign. When a diagnostic overlaps the selection, the same action is also offered as its quickfix.

func (EnumValuesProvider) Actions

func (p EnumValuesProvider) Actions(ctx context.Context, f File, span Span, report Report) []Action

type FieldIDCheck

type FieldIDCheck struct{}

func (*FieldIDCheck) AnalyzeFile

func (c *FieldIDCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*FieldIDCheck) Name

func (c *FieldIDCheck) Name() string

FieldIDCheck checks struct, union, exception, function parameter, and throws field ids: they must be unique positive integers in [1, 32767].

type FieldQualifierProvider

type FieldQualifierProvider struct{}

FieldQualifierProvider offers the "Make field required" and "Make field optional" actions for the fields whose declaration line the selection covers. Every covered field yields one action per qualifier it does not already carry: an unqualified field gets both, a required field gets "Make field optional", and vice versa. Union fields never offer "Make field required": unions have no required members.

func (FieldQualifierProvider) Actions

func (p FieldQualifierProvider) Actions(ctx context.Context, f File, span Span, report Report) []Action

type File

type File struct {
	URI uri.URI
	PF  *store.ParsedFile
	// contains filtered or unexported fields
}

File is one file's analysis inputs: its parsed tree plus the run's shared state.

func (File) Index

func (f File) Index() *Index

Index returns the run's shared cross-file resolver, memoized across every analyzer in the run.

func (File) View

func (f File) View() Graph

View returns the run's view (include resolver, dependency graph).

type FileAnalyzer

type FileAnalyzer interface {
	Name() string
	AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)
}

FileAnalyzer is the per-file shape most checks take: findings depend only on the file itself. The runner loops files, skips unparseable ones, and collects read errors; the analyzer never sees the loop.

type Fix

type Fix struct {
	Title string
	Edits []Edit
}

Fix is a named set of edits resolving one diagnostic. The title is what the client shows in its quickfix menu.

func Apply added in v0.2.0

func Apply(content []byte, fixes []Fix) (out []byte, applied, skipped []Fix, err error)

Apply returns content with the fixes applied. A fix is all-or-nothing: when any of its edits overlaps an accepted fix's edits — or its own — the whole fix is skipped. Insertions (empty spans) never conflict with each other; at the same offset they land in argument order, and just before a replacement starting there. applied and skipped partition fixes in argument order. An edit outside content is an error: offsets come from the same parse the fixes were computed on, so a out-of-range span is a fixer bug, not a condition to skip over.

type FixResult added in v0.2.0

type FixResult struct {
	// Applied is the number of fixes applied across all passes.
	Applied int
	// FixedFiles lists the files whose content changed, sorted by URI.
	FixedFiles []uri.URI
	// Skipped lists the fixes that could not apply.
	Skipped []SkippedFix
	// Passes is the number of pipeline runs.
	Passes int
	// Remaining is the last pass's report: the diagnostics left after the
	// final pass, fixable or not.
	Remaining Report
}

FixResult reports one FixAll run.

type Fixer

type Fixer interface {
	Fix(ctx context.Context, f File, d Diagnostic) []Fix
}

Fixer computes fixes for diagnostics reported by other analyzers, on demand — for fixes too expensive to compute during analysis (a workspace search per unresolved type, for instance). The fixer self-filters on the diagnostic's code and returns nothing when it has none.

type Graph added in v0.2.0

type Graph interface {
	Parse(ctx context.Context, file uri.URI) (*store.ParsedFile, error)
	Dependents(file uri.URI) []uri.URI
	Includers(file uri.URI) []uri.URI
	KnownFiles() []uri.URI
	Folder() uri.URI
	WalkFiles(ctx context.Context, root uri.URI, fn func(uri.URI) error) error
	Resolver() *store.Resolver
}

Graph is the read surface analysis needs from the document store: parse files and walk the include graph. Analyzers, fixers, and providers see only this; the store's write and concurrency surface (Update, Evict, generations) stays with the session owner, except for the batch fixer below.

type Hit

type Hit struct {
	File uri.URI
	Span Span   // parser coordinates; the frontend maps them
	Text string // as written: "User", "shared.User", "shared.thrift.User"

	// Kind is the grammar slot the reference sits in, so callers can tell
	// type hits from value hits (e.g. for highlight kinds).
	Kind store.RefKind
}

Hit is one reference occurrence of a name, with the qualifying text preserved so a rename can rewrite includes correctly.

type Include

type Include struct {
	// contains filtered or unexported fields
}

type IncludeShadowCheck

type IncludeShadowCheck struct{}

IncludeShadowCheck warns when one include path matches files under more than one include path. The nearest include path wins; the warning names the rest.

func (*IncludeShadowCheck) AnalyzeFile

func (c *IncludeShadowCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*IncludeShadowCheck) Name

func (c *IncludeShadowCheck) Name() string

type Index

type Index struct {
	// contains filtered or unexported fields
}

Index answers cross-file semantic queries over one view: definition resolution and reference search. It composes per-file store.FileIndexes over the include graph.

An Index is cheap — construct one per request with NewIndex. Resolutions are memoized per (file, name), so a request resolving the same name in the same file repeatedly (references, diagnostics) resolves it once.

func NewIndex

func NewIndex(view Graph) *Index

NewIndex returns an Index over the view's store.

func (*Index) FindInWorkspace

func (x *Index) FindInWorkspace(ctx context.Context, name string) (*Resolved, error)

FindInWorkspace returns the definition of name in any known file of the workspace, falling back to a directory walk when the workspace has not been indexed yet (e.g. a quick-fix on the first didOpen).

func (*Index) References

func (x *Index) References(ctx context.Context, file uri.URI, name string, kinds ...store.RefKind) ([]Hit, error)

References returns every occurrence of name in file and in every file that transitively includes it, restricted to the given reference kinds. The definition site is not included (no self-referencing hit). Hits are matched by bare name only; use ReferencesTo for resolution-matched results.

func (*Index) ReferencesTo

func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...store.RefKind) ([]Hit, error)

ReferencesTo returns every reference to def: in def.File and every file that transitively includes it. Hits are name- and resolution-matched, so same-named definitions elsewhere are not reported.

For an enum def, value references qualified with the enum name ("Color.RED", "shared.Color.RED") are matched too, provided the qualifier resolves to this very enum; the hit covers only the enum segment, so a rename rewrites the qualifier while keeping the member name.

func (*Index) ReferencingFiles

func (x *Index) ReferencingFiles(file uri.URI) []uri.URI

ReferencingFiles returns every file that directly includes file, in graph order.

func (*Index) ResolveService

func (x *Index) ResolveService(ctx context.Context, from *store.ParsedFile, ident *syntax.Identifier) (*Resolved, error)

ResolveService resolves a service name or extends reference, or returns nil when unresolved.

func (*Index) ResolveType

func (x *Index) ResolveType(ctx context.Context, from *store.ParsedFile, ft *syntax.FieldType) (*Resolved, error)

ResolveType resolves a type reference to its definition, or returns nil when unresolved (base types, unresolvable name). Files backing the resolution may fail to read; those count as unresolved.

func (*Index) ResolveValue

func (x *Index) ResolveValue(ctx context.Context, from *store.ParsedFile, v *syntax.ConstValue) (*Resolved, error)

ResolveValue resolves a const-value identifier to its definition (an enum value or a const), or returns nil when unresolved.

func (*Index) UnderlyingType

func (x *Index) UnderlyingType(ctx context.Context, from *store.ParsedFile, ft *syntax.FieldType) (*syntax.FieldType, *store.ParsedFile)

UnderlyingType follows typedef chains — across includes — until it reaches a type that is not itself a typedef, returning that type and the parsed file whose scope it resolves in. Every consumer that classifies a type by kind (value matching, completion, checks) resolves through here instead of comparing surface names, so "typedef map<string,string> M" classifies as a map everywhere.

An identifier that resolves to no definition returns nil — the type has no classifiable kind, and existence is another check's job. A cyclic or pathological chain stops at maxTypedefDepth steps.

func (*Index) View

func (x *Index) View() Graph

View returns the store view the index reads from.

type NonScalarMapKeyCheck added in v0.2.0

type NonScalarMapKeyCheck struct{}

NonScalarMapKeyCheck reports map key types that are not scalar or enum types. Structs, unions, exceptions, and containers are not valid map keys in standard Thrift, while typedefs are followed to their underlying type.

func (*NonScalarMapKeyCheck) AnalyzeFile added in v0.2.0

func (c *NonScalarMapKeyCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*NonScalarMapKeyCheck) Name added in v0.2.0

func (c *NonScalarMapKeyCheck) Name() string

type ParseCheck

type ParseCheck struct{}

ParseCheck reports the lexer's and parser's errors and warnings for every changed file. It is a whole-run analyzer because it must see files whose AST is too broken for the other analyzers — they skip those, this one reports them.

func (*ParseCheck) Analyze

func (p *ParseCheck) Analyze(ctx context.Context, run *Run) error

func (*ParseCheck) Name

func (p *ParseCheck) Name() string

type Pipeline

type Pipeline struct {
	// contains filtered or unexported fields
}

Pipeline runs analyzers over changed files. It is a value: safe to share, no state beyond the analyzers, fixers, providers, and config.

func DefaultPipeline

func DefaultPipeline(cfg Config) *Pipeline

DefaultPipeline composes the built-in analyzers with their fixers and action providers.

func New

func New(cfg Config, analyzers []Analyzer) *Pipeline

New composes a pipeline. Composition lives at the caller: no global registry.

func (*Pipeline) CodeActions

func (p *Pipeline) CodeActions(ctx context.Context, view Graph, file uri.URI, span Span, report Report) []Action

CodeActions returns the actions for span in file: quickfixes from the report's diagnostics overlapping span (inline fixes first, then the fixers), then the action providers' refactors.

func (*Pipeline) FixAll added in v0.2.0

func (p *Pipeline) FixAll(ctx context.Context, view Store, targets []uri.URI, persist func(context.Context, uri.URI, []byte) error) (FixResult, error)

FixAll runs the pipeline over targets and applies every applicable fix, re-running until a pass applies no fix or maxFixPasses passes have run. Only targets are analyzed and fixed; resolution and fixers read the whole view, so fixing one file of a workspace resolves against all of it and never touches the other files.

persist receives each changed file's new content: it must make the content durable (write to disk, update the editor overlay) and visible to view's file source before returning. FixAll drives view.Update itself, so persist must not.

func (*Pipeline) FixesForFile added in v0.2.0

func (p *Pipeline) FixesForFile(ctx context.Context, view Graph, file uri.URI, report Report) []Fix

FixesForFile returns every fix the pipeline offers for file: the inline fixes of the report's diagnostics for it, then the fixers' output for each diagnostic. Fixes are edits in parser coordinates on the file's current content.

func (*Pipeline) Run

func (p *Pipeline) Run(ctx context.Context, view Graph, changed []uri.URI) (Report, error)

Run analyzes changed over view: one run, one shared Index across all changed files and all analyzers.

func (*Pipeline) WithAnalyzers added in v0.2.0

func (p *Pipeline) WithAnalyzers(analyzers ...Analyzer) *Pipeline

WithAnalyzers returns a copy of the pipeline with analyzers appended.

func (*Pipeline) WithFixers

func (p *Pipeline) WithFixers(fs ...Fixer) *Pipeline

WithFixers returns a copy of the pipeline with the fixers added.

func (*Pipeline) WithProviders

func (p *Pipeline) WithProviders(ps ...ActionProvider) *Pipeline

WithProviders returns a copy of the pipeline with the action providers added.

type Report

type Report map[uri.URI][]Diagnostic

Report is the result of one analysis pass, keyed by file.

type Resolved

type Resolved struct {
	File   uri.URI
	Parsed *store.ParsedFile

	// Name is the definition's identifier node, whose range is the jump
	// target.
	Name *syntax.Identifier

	// Node is the definition itself: *syntax.Struct, *syntax.Enum, etc.
	// For an enum value, Node is the *syntax.Identifier (same as Name).
	Node syntax.Node

	Kind DefinitionKind
}

Resolved is a resolved definition: the target file, the parsed document, the definition identifier (jump target), and its kind.

func DefFromNode

func DefFromNode(pf *store.ParsedFile, n syntax.Node) *Resolved

DefFromNode builds a Resolved from any top-level definition node. Use when the concrete type and Kind are not known statically (e.g. FindInWorkspace).

type Run

type Run struct {
	// contains filtered or unexported fields
}

Run is one analysis pass. Analyzers add findings to it.

func (*Run) Add

func (r *Run) Add(file uri.URI, ds ...Diagnostic)

Add appends findings for file, applying the run's severity overrides. The findings are copied: the caller keeps ownership of its slice.

func (*Run) Files

func (r *Run) Files() []uri.URI

Files returns the files the run analyzes.

func (*Run) Index added in v0.2.1

func (r *Run) Index() *Index

Index returns the run's shared cross-file resolver, memoized across every analyzer in the run.

func (*Run) View added in v0.2.1

func (r *Run) View() Graph

View returns the run's view (include resolver, dependency graph).

type SemanticAnalysis

type SemanticAnalysis struct{}

func (*SemanticAnalysis) AnalyzeFile

func (s *SemanticAnalysis) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*SemanticAnalysis) Name

func (s *SemanticAnalysis) Name() string

type Severity

type Severity uint8

Severity is a diagnostic's display weight.

const (
	SeverityError Severity = iota + 1
	SeverityWarning
	SeverityInfo
	SeverityHint
)

type SkippedFix added in v0.2.0

type SkippedFix struct {
	File   uri.URI
	Fix    Fix
	Reason string
}

SkippedFix is a fix FixAll could not apply.

type Span

type Span struct {
	Start, End syntax.Position
}

Span is a half-open file region in parser coordinates (1-based line and rune column, byte offset). The byte offsets are authoritative; the LSP frontend maps them through the file mapper to UTF-16.

func SpanOf

func SpanOf(pf *store.ParsedFile, node syntax.Node) Span

SpanOf returns the source span of a node in the parsed file.

func (Span) Overlaps

func (s Span) Overlaps(o Span) bool

Overlaps reports whether s and o share at least one position; a cursor at either endpoint counts.

type Store added in v0.2.0

type Store interface {
	Graph
	Update(ctx context.Context, changes ...*store.FileChange) store.ChangeResult
}

Store is the read-write surface batch fixing needs: analyze through Graph, land each pass through Update.

type UnusedIncludeCheck

type UnusedIncludeCheck struct{}

UnusedIncludeCheck reports an include no reference in the file resolves into. Unused includes bloat the compile and make the dependency graph look worse than it is.

func (*UnusedIncludeCheck) AnalyzeFile

func (c *UnusedIncludeCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, error)

func (*UnusedIncludeCheck) Name

func (c *UnusedIncludeCheck) Name() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL