diff --git a/cmd/lsif-test/args.go b/cmd/lsif-test/args.go new file mode 100644 index 0000000..958ebaf --- /dev/null +++ b/cmd/lsif-test/args.go @@ -0,0 +1,34 @@ +package main + +import ( + "os" + + "github.com/alecthomas/kingpin" +) + +var app = kingpin.New( + "lsif-test", + "lsif-test is test runner for validating LSIF indexer output.", +).Version(version) + +var ( + indexFile *os.File + testsFile *os.File +) + +func init() { + app.HelpFlag.Short('h') + app.VersionFlag.Short('v') + app.HelpFlag.Hidden() + + app.Arg("index-file", "The LSIF index to visualize.").Default("dump.lsif").FileVar(&indexFile) + app.Arg("tests-file", "The test specification file.").Default("tests.yaml").FileVar(&testsFile) +} + +func parseArgs(args []string) (err error) { + if _, err := app.Parse(args); err != nil { + return err + } + + return nil +} diff --git a/cmd/lsif-test/internal/runner/context.go b/cmd/lsif-test/internal/runner/context.go new file mode 100644 index 0000000..8b2c95b --- /dev/null +++ b/cmd/lsif-test/internal/runner/context.go @@ -0,0 +1,15 @@ +package runner + +import ( + "github.com/sourcegraph/lsif-test/internal/reader" +) + +type RunnerContext struct { + Stasher *reader.Stasher +} + +func NewRunnerContext() *RunnerContext { + return &RunnerContext{ + Stasher: reader.NewStasher(), + } +} diff --git a/cmd/lsif-test/internal/runner/runner.go b/cmd/lsif-test/internal/runner/runner.go new file mode 100644 index 0000000..6132090 --- /dev/null +++ b/cmd/lsif-test/internal/runner/runner.go @@ -0,0 +1,57 @@ +package runner + +import ( + "fmt" + "io" + "os" + + reader "github.com/sourcegraph/lsif-protocol/reader" + "github.com/sourcegraph/lsif-test/cmd/lsif-test/internal/search" + reader2 "github.com/sourcegraph/lsif-test/internal/reader" +) + +type Runner struct { + Context *RunnerContext +} + +func (v *Runner) Run(indexFile, testsFile io.Reader) error { + testSpecs, err := ReadTestSpecs(testsFile) + if err != nil { + return err + } + + if err := reader2.Read(indexFile, v.Context.Stasher, nil, nil); err != nil { + return err + } + + var vertices []reader.Element + _ = v.Context.Stasher.Vertices(func(lineContext reader2.LineContext) bool { + vertices = append(vertices, lineContext.Element) + return true + }) + + var edges []reader.Element + _ = v.Context.Stasher.Edges(func(lineContext reader2.LineContext, edge reader.Edge) bool { + edges = append(edges, lineContext.Element) + return true + }) + + for _, testSpec := range testSpecs { + rangeData := search.GatherRangeDataFromPosition(vertices, edges, testSpec.Path, testSpec.Line, testSpec.Character) + if len(rangeData) != 1 { + fmt.Printf("error: no or overlapping range data\n") + os.Exit(1) + } + + if rangeData[0].HoverText != testSpec.HoverText { + fmt.Printf("error: bad hover text\n") + os.Exit(1) + } + + // TODO - test definitions + // TODO - test references + // TODO - test monikers + } + + return nil +} diff --git a/cmd/lsif-test/internal/runner/spec.go b/cmd/lsif-test/internal/runner/spec.go new file mode 100644 index 0000000..4abdd2d --- /dev/null +++ b/cmd/lsif-test/internal/runner/spec.go @@ -0,0 +1,39 @@ +package runner + +import ( + "io" + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +type TestSpec struct { + Path string `yaml:"path"` + Line int `yaml:"line"` + Character int `yaml:"character"` + HoverText string `yaml:"hoverText"` + Definitions []Location `yaml:"definitions"` + References []Location `yaml:"references"` +} + +type Location struct { + Path string `yaml:"path"` + StartLine int `yaml:"startLine"` + StartCharacter int `yaml:"startCharacter"` + EndLine int `yaml:"endLine"` + EndCharacter int `yaml:"endCharacter"` +} + +func ReadTestSpecs(r io.Reader) ([]TestSpec, error) { + content, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + + var testSpecs []TestSpec + if err := yaml.Unmarshal(content, &testSpecs); err != nil { + return nil, err + } + + return testSpecs, nil +} diff --git a/cmd/lsif-test/internal/search/search.go b/cmd/lsif-test/internal/search/search.go new file mode 100644 index 0000000..77de59f --- /dev/null +++ b/cmd/lsif-test/internal/search/search.go @@ -0,0 +1,241 @@ +package search + +import ( + "strings" + + "github.com/sourcegraph/lsif-protocol/reader" +) + +type RangeData struct { + HoverText string + Definitions []Location + References []Location +} + +type Location struct { + Path string + StartLine int + StartCharacter int + EndLine int + EndCharacter int +} + +func GatherRangeDataFromPosition(vertices, edges []reader.Element, path string, line, character int) []RangeData { + var rangeData []RangeData + for _, rangeID := range findRangeByPosition(vertices, edges, path, line, character) { + rangeData = append(rangeData, gatherRangeDataFromID(vertices, edges, rangeID)) + } + + return rangeData +} + +func gatherRangeDataFromID(vertices, edges []reader.Element, rangeID int) RangeData { + return RangeData{ + HoverText: findHoverTextByRangeID(vertices, edges, rangeID), + Definitions: findDefinitionLocationsByRangeID(vertices, edges, rangeID), + References: findReferenceLocationsByRangeID(vertices, edges, rangeID), + } +} + +func findRangeByPosition(vertices, edges []reader.Element, path string, line, character int) []int { + projectRoot := getProjectRoot(vertices, edges) + + for _, vertex := range vertices { + if vertex.Label == "document" && strings.TrimPrefix(vertex.Payload.(string), projectRoot) == path { + return findOverlappingRanges(vertices, edges, findRangesContainedByDocument(vertices, edges, vertex.ID), line, character) + } + } + + return nil +} + +func getProjectRoot(vertices, edges []reader.Element) string { + for _, vertex := range vertices { + if vertex.Label == "metaData" { + projectRoot := vertex.Payload.(reader.MetaData).ProjectRoot + if !strings.HasSuffix(projectRoot, "/") { + projectRoot += "/" + } + + return projectRoot + } + } + + return "" +} + +func findRangesContainedByDocument(vertices, edges []reader.Element, documentID int) []int { + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "contains" && payload.OutV == documentID { + return payload.InVs + } + } + + return nil +} + +func findOverlappingRanges(vertices, edges []reader.Element, rangeIDs []int, line, character int) []int { + rangeIDMap := map[int]struct{}{} + for _, rangeID := range rangeIDs { + rangeIDMap[rangeID] = struct{}{} + } + + var intersectingRangeIDs []int + for _, vertex := range vertices { + if _, ok := rangeIDMap[vertex.ID]; ok && contains(vertex.Payload.(reader.Range), line, character) { + intersectingRangeIDs = append(intersectingRangeIDs, vertex.ID) + } + } + + return intersectingRangeIDs +} + +func contains(r reader.Range, line, character int) bool { + if r.StartLine > line || r.EndLine < line { + return false + } + if r.StartLine == line && r.StartCharacter > character { + return false + } + if r.EndLine == line && r.EndCharacter < character { + return false + } + + return true +} + +func findHoverTextByRangeID(vertices, edges []reader.Element, rangeID int) string { + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "textDocument/hover" && payload.OutV == rangeID { + return findHoverTextByID(vertices, edges, payload.InV) + } + } + + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "next" && payload.OutV == rangeID { + if hoverText := findHoverTextByRangeID(vertices, edges, payload.InV); hoverText != "" { + return hoverText + } + } + } + + return "" +} + +func findHoverTextByID(vertices, edges []reader.Element, hoverResultID int) string { + for _, vertex := range vertices { + if vertex.Label == "hoverResult" && vertex.ID == hoverResultID { + return vertex.Payload.(string) + } + } + + return "" +} + +// +// +// TODO +// +// + +func findDefinitionLocationsByRangeID(vertices, edges []reader.Element, rangeID int) []Location { + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "textDocument/definition" && payload.OutV == rangeID { + return findDefinitionLocationsByID(vertices, edges, payload.InV) + } + } + + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "next" && payload.OutV == rangeID { + if locations := findDefinitionLocationsByRangeID(vertices, edges, payload.InV); len(locations) > 0 { + return locations + } + } + } + + return nil +} + +func findDefinitionLocationsByID(vertices, edges []reader.Element, definitionResultID int) []Location { + var locations []Location + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "item" && payload.OutV == definitionResultID { + for _, inV := range payload.InVs { + locations = append(locations, findLocationByID(vertices, edges, inV)) + } + } + } + + return locations +} + +func findReferenceLocationsByRangeID(vertices, edges []reader.Element, rangeID int) []Location { + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "textDocument/references" && payload.OutV == rangeID { + return findReferenceLocationsByID(vertices, edges, payload.InV) + } + } + + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "next" && payload.OutV == rangeID { + if locations := findReferenceLocationsByRangeID(vertices, edges, payload.InV); len(locations) > 0 { + return locations + } + } + } + + return nil +} + +func findReferenceLocationsByID(vertices, edges []reader.Element, referenceResultID int) []Location { + var locations []Location + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "item" && payload.OutV == referenceResultID { + for _, inV := range payload.InVs { + locations = append(locations, findLocationByID(vertices, edges, inV)) + } + } + } + + return locations +} + +func findLocationByID(vertices, edges []reader.Element, rangeID int) Location { + for _, vertex := range vertices { + if vertex.Label == "range" && vertex.ID == rangeID { + r := vertex.Payload.(reader.Range) + + return Location{ + Path: findURIContaining(vertices, edges, rangeID), + StartLine: r.StartLine, + StartCharacter: r.StartCharacter, + EndLine: r.EndLine, + EndCharacter: r.EndCharacter, + } + } + } + + return Location{} +} + +func findURIContaining(vertices, edges []reader.Element, rangeID int) string { + for _, edge := range edges { + if payload := edge.Payload.(reader.Edge); edge.Label == "contains" { + for _, inV := range payload.InVs { + if inV == rangeID { + projectRoot := getProjectRoot(vertices, edges) + + for _, vertex := range vertices { + if vertex.Label == "document" && vertex.ID == payload.OutV { + return strings.TrimPrefix(vertex.Payload.(string), projectRoot) + } + } + + return "" + } + } + } + } + + return "" +} diff --git a/cmd/lsif-test/main.go b/cmd/lsif-test/main.go new file mode 100644 index 0000000..c9bdb28 --- /dev/null +++ b/cmd/lsif-test/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "fmt" + "os" +) + +const version = "0.1.0" + +func main() { + if err := mainErr(); err != nil { + fmt.Fprint(os.Stderr, fmt.Sprintf("\nerror: %v\n", err)) + os.Exit(1) + } +} + +func mainErr() error { + if err := parseArgs(os.Args[1:]); err != nil { + return err + } + defer indexFile.Close() + defer testsFile.Close() + + return test(indexFile, testsFile) +} diff --git a/cmd/lsif-test/test.go b/cmd/lsif-test/test.go new file mode 100644 index 0000000..7de2318 --- /dev/null +++ b/cmd/lsif-test/test.go @@ -0,0 +1,13 @@ +package main + +import ( + "os" + + "github.com/sourcegraph/lsif-test/cmd/lsif-test/internal/runner" +) + +func test(indexFile, testsFile *os.File) error { + ctx := runner.NewRunnerContext() + runner := &runner.Runner{Context: ctx} + return runner.Run(indexFile, testsFile) +} diff --git a/cmd/lsif-test/tests.yaml b/cmd/lsif-test/tests.yaml new file mode 100644 index 0000000..9e3a8f1 --- /dev/null +++ b/cmd/lsif-test/tests.yaml @@ -0,0 +1,19 @@ +- path: protocol/emitter.go + line: 101 + character: 12 + hoverText: | + foo + bar + baz + bonk + definitions: + - path: foo + startLine: 3 + startCharacter: 4 + endLine: 3 + endCharacter: 6 + - path: bar + startLine: 4 + startCharacter: 5 + endLine: 4 + endCharacter: 7 diff --git a/go.mod b/go.mod index d67bc08..50710cc 100644 --- a/go.mod +++ b/go.mod @@ -10,4 +10,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/sourcegraph/lsif-protocol v1.0.0 + golang.org/x/tools v0.0.0-20190428024724-550556f78a90 + gopkg.in/yaml.v2 v2.2.5 ) diff --git a/go.sum b/go.sum index a823ab0..f8fa536 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,7 @@ golang.org/x/sys v0.0.0-20190428183149-804c0c7841b5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190428024724-550556f78a90 h1:oGQGZoUHCvTWJLWXu4Qpp6uMF6gpOAGVm1hZx1KJkhU= golang.org/x/tools v0.0.0-20190428024724-550556f78a90/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=