Something went wrong. Try again.
A Thrift language server, formatter, and linter.
lsp linter helix vim vscode thrift
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195package sema
import ( "context" "fmt" "slices"
"go.lsp.dev/uri"
"github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax")
// maxFixPasses bounds FixAll's fixpoint loop. One pass may unlock another// (adding an include resolves an undefined type, whose fix may unlock a// third); ten passes mean the fixes oscillate, which is a fixer bug.const maxFixPasses = 10
// 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 (p *Pipeline) FixesForFile(ctx context.Context, view Graph, file uri.URI, report Report) []Fix { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil }
f := File{URI: file, PF: pf, run: &Run{view: view, cfg: p.cfg, report: report}}
return p.fixesFor(ctx, f, report[file])}
// fixesFor collects the fixes for one file's diagnostics: inline fixes// first, then the fixers' output per diagnostic, in diagnostic order.func (p *Pipeline) fixesFor(ctx context.Context, f File, ds []Diagnostic) []Fix { var out []Fix
for _, d := range ds { out = append(out, d.Fixes...)
for _, fx := range p.fixers { out = append(out, fx.Fix(ctx, f, d)...) } }
return out}
// SkippedFix is a fix FixAll could not apply.type SkippedFix struct { File uri.URI Fix Fix Reason string}
// FixResult reports one FixAll run.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}
// 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 (p *Pipeline) FixAll(ctx context.Context, view Store, targets []uri.URI, persist func(context.Context, uri.URI, []byte) error) (FixResult, error) { var res FixResult
targets = slices.Clone(targets) slices.Sort(targets)
changed := make(map[uri.URI]struct{}, len(targets))
// summary assembles the result on every exit path: a persist failure // may leave earlier files of the same pass already mutated, and the // caller must be able to report that. summary := func() FixResult { res.FixedFiles = make([]uri.URI, 0, len(changed)) for u := range changed { res.FixedFiles = append(res.FixedFiles, u) }
slices.Sort(res.FixedFiles)
return res }
for pass := range maxFixPasses { res.Passes++
report, err := p.Run(ctx, view, targets) if err != nil { return summary(), err }
res.Remaining = report
changes := make([]*store.FileChange, 0, len(targets)) passSkipped := make([]SkippedFix, 0, len(res.Skipped)) applied := 0
for _, u := range targets { pf, err := view.Parse(ctx, u) if err != nil { continue }
// A partial AST's spans are content-derived, but a // mis-attributed node could delete the wrong line: batch // fixing never touches files whose parse reported errors. // Warning-only parse output parses completely and fixes on. // Their diagnostics stay in Remaining. if slices.ContainsFunc(pf.Errors(), func(e syntax.Error) bool { return e.Severity == syntax.SeverityError }) { for _, d := range report[u] { for _, fx := range d.Fixes { passSkipped = append(passSkipped, SkippedFix{File: u, Fix: fx, Reason: "file has parse errors"}) } }
continue }
fixes := p.FixesForFile(ctx, view, u, report) if len(fixes) == 0 { continue }
content, err := pf.Content() if err != nil { return summary(), err }
out, ok, skip, err := Apply(content, fixes) if err != nil { return summary(), fmt.Errorf("%s: %w", u.FsPath(), err) }
for _, fx := range skip { passSkipped = append(passSkipped, SkippedFix{File: u, Fix: fx, Reason: "overlaps another fix"}) }
if len(ok) == 0 { continue }
// Count and record only after the content is durable: a // failed persist leaves the file unmutated on disk, and the // summary must report what actually landed. if persist != nil { if err := persist(ctx, u, out); err != nil { return summary(), fmt.Errorf("%s: %w", u.FsPath(), err) } }
applied += len(ok) res.Applied += len(ok)
changes = append(changes, &store.FileChange{URI: u, Version: pass + 1, Content: out, From: store.FileChangeTypeDidChange}) changed[u] = struct{}{} }
// Only the final pass's skips are reported: earlier passes' // skips were recomputed against fresh ranges the next pass. if applied == 0 || pass == maxFixPasses-1 { res.Skipped = passSkipped }
if applied == 0 { break }
view.Update(ctx, changes...) }
return summary(), nil}