Something went wrong. Try again.
A Thrift language server, formatter, and linter.
lsp linter helix vim vscode thrift
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232package check
import ( "os" "path/filepath" "strings" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
"github.com/karitham/thrift-ls/options")
// corpusDir is the shared lint corpus, one intentional mistake per section// in lints.thrift, an include cycle on the cycle_a/b pair, and no// diagnostics on the clean files.func corpusDir(t *testing.T) string { t.Helper()
abs, err := filepath.Abs(filepath.Join("..", "tests", "made-in-abyss")) require.NoError(t, err)
return abs}
// corpusFiles returns the absolute paths of the corpus files in lexical// order.func corpusFiles(t *testing.T) []string { t.Helper()
entries, err := os.ReadDir(corpusDir(t)) require.NoError(t, err)
var files []string
for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".thrift") { files = append(files, filepath.Join(corpusDir(t), e.Name())) } }
return files}
// Test_CheckMadeInAbyss pins the lint corpus through the public boundary:// the mistake showcase, the include cycle, and the clean files.func Test_CheckMadeInAbyss(t *testing.T) { ctx := t.Context() root := corpusDir(t) files := corpusFiles(t)
result, err := Run(ctx, Request{Files: files, Folder: root}) require.NoError(t, err)
diags := result.Diagnostics assert.Nil(t, result.Fix)
// The clean corpus files report nothing. for _, name := range []string{"abyss.thrift", "delvers.thrift", "orth.thrift"} { assert.Empty(t, diags[filepath.Join(root, name)], "%s must be clean", name) }
// The mutual include cycle: one warning on cycle_a (its include of // cycle_b closes the cycle), two on cycle_b. cycleA := diags[filepath.Join(root, "cycle_a.thrift")] require.Len(t, cycleA, 1) assert.Contains(t, cycleA[0].Message, "cycle dependency")
cycleB := diags[filepath.Join(root, "cycle_b.thrift")] require.Len(t, cycleB, 2) assert.Contains(t, cycleB[0].Message, "cycle dependency") assert.Contains(t, cycleB[1].Message, "cycle dependency")
// The mistake showcase: 18 errors and 7 warnings. lints := diags[filepath.Join(root, "lints.thrift")] require.Len(t, lints, 25)
errs, warns := 0, 0 for _, d := range lints { switch d.Severity { case SeverityError: errs++ case SeverityWarning: warns++ } } assert.Equal(t, 18, errs) assert.Equal(t, 7, warns) for _, d := range lints { if strings.Contains(d.Message, "map key must be a scalar type") { assert.Equal(t, SeverityWarning, d.Severity) } }
// Every intentional mistake is reported. for _, msg := range []string{ `unused include "unused.thrift"`, "field id conflict", "field id should be a positive integer in [1, 32767]", "duplicate enum Reg", "duplicate field same_name", "duplicate field repeat", "duplicate member RIKO", "enum value 1 duplicates OZEN", "duplicate function descend", "duplicate argument depth", `duplicate map key "zone1"`, "duplicate set value 4", "field type doesn't exist", "map key must be a scalar type, found struct", "STAR_COMPASS has no explicit value (implicitly 0)", "UNHEARD_BELL has no explicit value (implicitly 3)", "CROSSED_STILLS has no explicit value (implicitly 5)", } { assert.True(t, hasMessage(lints, msg), "missing diagnostic %q", msg) }
// Two checks on one line: the second `repeat` field conflicts on both // its id (FieldIDCheck) and its name (DuplicateCheck). var repeatLine int for _, d := range lints { if strings.Contains(d.Message, "duplicate field repeat") { repeatLine = d.Line } }
onLine := diagsOnLine(lints, repeatLine) require.Len(t, onLine, 2, "line %d must carry both diagnostics", repeatLine) assert.Contains(t, onLine[0].Message, "field id conflict") assert.Contains(t, onLine[1].Message, "duplicate field repeat")}
// hasMessage reports whether any diagnostic carries msg.func hasMessage(diags []Diagnostic, msg string) bool { for _, d := range diags { if strings.Contains(d.Message, msg) { return true } }
return false}
// diagsOnLine returns the diagnostics starting on the 1-based line.func diagsOnLine(diags []Diagnostic, line int) []Diagnostic { var out []Diagnostic
for _, d := range diags { if d.Line == line { out = append(out, d) } }
return out}
// Test_Check_LintConfig pins that lint settings reach the pipeline: a// disabled analyzer produces no diagnostics, while the default// configuration reports the unused include.func Test_Check_LintConfig(t *testing.T) { folder := t.TempDir() file := filepath.Join(folder, "user.thrift") content := "include \"shared.thrift\"\nstruct S { 1: i32 a }\n" require.NoError(t, os.WriteFile(file, []byte(content), 0o644)) t.Setenv("THRIFT_LS_CONFIG", "")
config := `{"lint": {"disabled": ["UnusedIncludeCheck"]}}` configPath := filepath.Join(folder, "thrift-ls.json") require.NoError(t, os.WriteFile(configPath, []byte(config), 0o644))
patch, err := options.Load(configPath) require.NoError(t, err)
result, err := Run(t.Context(), Request{Files: []string{file}, Folder: folder, Lint: options.Effective(patch).Lint}) require.NoError(t, err) assert.Empty(t, result.Diagnostics[file])
// Without the config the warning fires. result, err = Run(t.Context(), Request{Files: []string{file}, Folder: folder}) require.NoError(t, err) assert.True(t, hasMessage(result.Diagnostics[file], "unused include"))}
func Test_Check_FixConverges(t *testing.T) { folder := t.TempDir() file := filepath.Join(folder, "user.thrift") require.NoError(t, os.WriteFile(file, []byte("enum Color {\n RED,\n GREEN = 2,\n BLUE,\n}\n"), 0o644))
result, err := Run(t.Context(), Request{Files: []string{file}, Folder: folder, Fix: true}) require.NoError(t, err) require.NotNil(t, result.Fix, "a fix run reports its summary") assert.Positive(t, result.Fix.Applied) assert.Positive(t, result.Fix.Passes) assert.Contains(t, result.Fix.Files, file)
for _, d := range result.Diagnostics[file] { assert.NotContains(t, d.Message, "explicit value", "remaining diagnostics hold only what fixes cannot do") }
fixed, err := os.ReadFile(file) require.NoError(t, err) assert.Contains(t, string(fixed), "RED = 0")}
func Test_Check_EmptyInput(t *testing.T) { result, err := Run(t.Context(), Request{}) require.NoError(t, err) assert.NotNil(t, result.Diagnostics) assert.Empty(t, result.Diagnostics)}
func Test_Check_SeverityOverride(t *testing.T) { folder := t.TempDir() file := filepath.Join(folder, "user.thrift") require.NoError(t, os.WriteFile(file, []byte("include \"shared.thrift\"\nstruct S { 1: i32 a }\n"), 0o644))
sev := map[string]string{"unused-include": "error"} result, err := Run(t.Context(), Request{ Files: []string{file}, Folder: folder, Lint: &options.LintConfig{Severity: &sev}, }) require.NoError(t, err) require.NotEmpty(t, result.Diagnostics[file]) for _, d := range result.Diagnostics[file] { if d.Code == "unused-include" { assert.Equal(t, SeverityError, d.Severity) } }}