├── .gitattributes ├── scripts ├── create_and_diff.sh └── quickstart.sh ├── object ├── graph.go ├── impact_global.go ├── ctx_fact.go ├── impact.go ├── source_context.go └── ctx_rel.go ├── parser ├── lsif │ ├── testdata │ │ ├── dump.lsif.zip │ │ └── expected │ │ │ └── lsif │ │ │ ├── main.go.json │ │ │ └── morestrings │ │ │ └── reverse.go.json │ ├── README.md │ ├── cache_test.go │ ├── id_test.go │ ├── hovers_test.go │ ├── id.go │ ├── performance_test.go │ ├── references_test.go │ ├── ranges_test.go │ ├── docs_test.go │ ├── parser_test.go │ ├── parser.go │ ├── references.go │ ├── cache.go │ ├── hovers.go │ ├── docs.go │ ├── file.go │ ├── code_hover_test.go │ ├── code_hover.go │ └── ranges.go └── api_test.go ├── graph ├── common │ ├── edge.go │ └── options.go ├── function │ ├── api_query_file.go │ ├── api_modify.go │ ├── api_stat_test.go │ ├── api_modify_test.go │ ├── api_draw.go │ ├── api_storage_test.go │ ├── api_draw_test.go │ ├── api_draw_dot.go │ ├── vertex.go │ ├── api_query.go │ ├── api_query_test.go │ ├── api_draw_g6.go │ ├── fact.go │ ├── api_transform.go │ ├── api_query_ctx.go │ ├── api_stat.go │ ├── api_storage.go │ └── graph.go ├── file │ ├── api_modify.go │ ├── vertex.go │ ├── api_draw.go │ ├── graph_test.go │ ├── api_query.go │ ├── api_query_ctx_test.go │ ├── api_transform.go │ ├── api_test.go │ ├── api_query_ctx.go │ ├── api_draw_g6.go │ ├── api_stat.go │ └── graph.go ├── utils │ └── modify.go └── visual │ └── g6 │ ├── object.go │ └── template │ └── report.html ├── diff ├── git_test.go └── git.go ├── cmd └── srctx │ ├── diff │ ├── core_test.go │ ├── core.go │ ├── objects.go │ ├── cmd.go │ ├── core_file.go │ └── core_func.go │ ├── main.go │ ├── main_test.go │ └── dump │ └── cmd.go ├── .github └── workflows │ ├── imagebuild.yml │ ├── release.yml │ └── ci.yml ├── version.go ├── Makefile ├── .gitignore ├── matrix ├── api_test.go └── api.go ├── Dockerfile ├── example ├── api_base_test.go └── api_test.go ├── go.mod └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /scripts/create_and_diff.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | cd /app 5 | lsif-go -v 6 | srctx diff 7 | -------------------------------------------------------------------------------- /object/graph.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | const ( 4 | NodeLevelFile = "file" 5 | NodeLevelFunc = "func" 6 | ) 7 | -------------------------------------------------------------------------------- /parser/lsif/testdata/dump.lsif.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/williamfzc/srctx/HEAD/parser/lsif/testdata/dump.lsif.zip -------------------------------------------------------------------------------- /parser/lsif/README.md: -------------------------------------------------------------------------------- 1 | # LSIF Parser 2 | 3 | Based on 4 | https://gitlab.com/gitlab-org/gitlab/-/tree/master/workhorse/internal/lsif_transformer 5 | -------------------------------------------------------------------------------- /graph/common/edge.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type EdgeStorage struct { 4 | RefLines map[int]struct{} 5 | } 6 | 7 | func NewEdgeStorage() *EdgeStorage { 8 | return &EdgeStorage{RefLines: make(map[int]struct{})} 9 | } 10 | -------------------------------------------------------------------------------- /diff/git_test.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGit(t *testing.T) { 10 | _, err := GitDiff("../", "HEAD~1", "HEAD") 11 | assert.Nil(t, err) 12 | } 13 | -------------------------------------------------------------------------------- /graph/function/api_query_file.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | func (fg *Graph) ListFiles() []string { 4 | ret := make([]string, 0, len(fg.Cache)) 5 | for k := range fg.Cache { 6 | ret = append(ret, k) 7 | } 8 | return ret 9 | } 10 | 11 | func (fg *Graph) FileCount() int { 12 | return len(fg.ListFiles()) 13 | } 14 | -------------------------------------------------------------------------------- /graph/file/api_modify.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/williamfzc/srctx/graph/utils" 5 | ) 6 | 7 | func (fg *Graph) RemoveNodeById(path string) error { 8 | err := utils.RemoveFromGraph(fg.G, path) 9 | if err != nil { 10 | return err 11 | } 12 | err = utils.RemoveFromGraph(fg.Rg, path) 13 | if err != nil { 14 | return err 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /graph/function/api_modify.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/williamfzc/srctx/graph/utils" 5 | ) 6 | 7 | func (fg *Graph) RemoveNodeById(funcId string) error { 8 | err := utils.RemoveFromGraph(fg.g, funcId) 9 | if err != nil { 10 | return err 11 | } 12 | err = utils.RemoveFromGraph(fg.rg, funcId) 13 | if err != nil { 14 | return err 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /cmd/srctx/diff/core_test.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCollectLineMap(t *testing.T) { 11 | impactLineMap, err := collectLineMap(&Options{ 12 | Src: ".", 13 | Before: "HEAD~1", 14 | After: "HEAD", 15 | }) 16 | assert.Nil(t, err) 17 | log.Debugf("map: %v", impactLineMap) 18 | } 19 | -------------------------------------------------------------------------------- /object/impact_global.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | /* 4 | StatGlobal 5 | 6 | designed for offering a global view 7 | */ 8 | type StatGlobal struct { 9 | UnitLevel string `json:"unitLevel,omitempty"` 10 | UnitMapping map[string]int `json:"unitMapping,omitempty"` 11 | 12 | ImpactUnits []int `json:"impactUnits,omitempty"` 13 | TransImpactUnits []int `json:"transImpactUnits,omitempty"` 14 | 15 | TotalEntries []int `json:"entries,omitempty"` 16 | ImpactEntries []int `json:"impactEntries,omitempty"` 17 | 18 | ImpactUnitsMap map[int]*ImpactUnit `json:"-"` 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/imagebuild.yml: -------------------------------------------------------------------------------- 1 | name: imagebuild 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | imagebuild: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Pack image 17 | run: 18 | docker build -t williamfzc/srctx:$GITHUB_REF_NAME . 19 | - name: Upload image 20 | run: | 21 | docker login -u williamfzc -p ${{ secrets.DOCKER_TOKEN }} 22 | docker push williamfzc/srctx:$GITHUB_REF_NAME 23 | -------------------------------------------------------------------------------- /graph/file/vertex.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | type Vertex struct { 4 | Path string 5 | Referenced int 6 | 7 | // https://github.com/williamfzc/srctx/issues/41 8 | Tags map[string]struct{} `json:"tags,omitempty"` 9 | } 10 | 11 | func (fv *Vertex) Id() string { 12 | return fv.Path 13 | } 14 | 15 | func (fv *Vertex) ContainTag(tag string) bool { 16 | if _, ok := fv.Tags[tag]; ok { 17 | return true 18 | } 19 | return false 20 | } 21 | 22 | func (fv *Vertex) AddTag(tag string) { 23 | fv.Tags[tag] = struct{}{} 24 | } 25 | 26 | func (fv *Vertex) RemoveTag(tag string) { 27 | delete(fv.Tags, tag) 28 | } 29 | -------------------------------------------------------------------------------- /graph/common/options.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/opensibyl/sibyl2/pkg/core" 4 | 5 | type GraphOptions struct { 6 | Src string `json:"src"` 7 | LsifFile string `json:"lsifFile"` 8 | ScipFile string `json:"scipFile"` 9 | Lang core.LangType `json:"lang"` 10 | 11 | // other options (like performance 12 | GenGolangIndex bool `json:"genGolangIndex"` 13 | NoEntries bool `json:"noEntries"` 14 | } 15 | 16 | func DefaultGraphOptions() *GraphOptions { 17 | return &GraphOptions{ 18 | Src: ".", 19 | LsifFile: "./dump.lsif", 20 | ScipFile: "./index.scip", 21 | Lang: core.LangUnknown, 22 | NoEntries: false, 23 | GenGolangIndex: false, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 williamfzc 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package srctx 18 | 19 | const ( 20 | Version = "v0.11.2" 21 | RepoUrl = "https://github.com/williamfzc/srctx" 22 | ) 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # General 2 | WORKDIR = $(PWD) 3 | 4 | # Go parameters 5 | GOCMD = go 6 | GOTEST = $(GOCMD) test 7 | 8 | default: 9 | go build ./cmd/srctx 10 | 11 | fmt: 12 | gofumpt -l -w . 13 | 14 | # linux 15 | build_linux_amd64: 16 | CGO_ENABLED=1 GOOS=linux GOARCH=amd64 ${GOCMD} build -o srctx_linux_amd64 ./cmd/srctx 17 | 18 | # windows 19 | build_windows_amd64: 20 | CGO_ENABLED=1 GOOS=windows GOARCH=amd64 ${GOCMD} build -o srctx_windows_amd64.exe ./cmd/srctx 21 | 22 | # mac 23 | build_macos_amd64: 24 | CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 ${GOCMD} build -o srctx_macos_amd64 ./cmd/srctx 25 | build_macos_arm64: 26 | CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 ${GOCMD} build -o srctx_macos_arm64 ./cmd/srctx 27 | 28 | test: 29 | $(GOTEST) ./... 30 | -------------------------------------------------------------------------------- /parser/lsif/cache_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type chunk struct { 11 | A int16 12 | B int16 13 | } 14 | 15 | func TestCache(t *testing.T) { 16 | cache, err := newCache("test-chunks", chunk{}) 17 | require.NoError(t, err) 18 | defer cache.Close() 19 | 20 | c := chunk{A: 1, B: 2} 21 | require.NoError(t, cache.SetEntry(1, &c)) 22 | require.NoError(t, cache.setOffset(0)) 23 | 24 | content, err := io.ReadAll(cache.GetReader()) 25 | require.NoError(t, err) 26 | 27 | expected := []byte{0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x2, 0x0} 28 | require.Equal(t, expected, content) 29 | 30 | var nc chunk 31 | require.NoError(t, cache.Entry(1, &nc)) 32 | require.Equal(t, c, nc) 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # idea 24 | .idea 25 | 26 | # macos 27 | .Ds_Store 28 | 29 | # temp data 30 | cmd/srctx/*.json 31 | cmd/srctx/*.csv 32 | cmd/srctx/*.html 33 | cmd/srctx/*.dot 34 | /*.lsif 35 | /src_darwin_amd64 36 | /srctx 37 | -------------------------------------------------------------------------------- /graph/utils/modify.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/dominikbraun/graph" 4 | 5 | func RemoveFromGraph[T string, U any](g graph.Graph[T, *U], hash T) error { 6 | // to this hash 7 | pm, err := g.PredecessorMap() 8 | if err != nil { 9 | return err 10 | } 11 | // from this hash 12 | am, err := g.AdjacencyMap() 13 | if err != nil { 14 | return err 15 | } 16 | 17 | v, ok := pm[hash] 18 | if ok { 19 | for s := range v { 20 | err = g.RemoveEdge(s, hash) 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | } 26 | 27 | v, ok = am[hash] 28 | if ok { 29 | for s := range v { 30 | err = g.RemoveEdge(hash, s) 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | } 36 | 37 | err = g.RemoveVertex(hash) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /graph/function/api_stat_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/opensibyl/sibyl2/pkg/core" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestStat(t *testing.T) { 13 | _, curFile, _, _ := runtime.Caller(0) 14 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 15 | fg, err := CreateFuncGraphFromDirWithLSIF(src, filepath.Join(src, "dump.lsif"), core.LangGo) 16 | assert.Nil(t, err) 17 | assert.NotEmpty(t, fg.Cache) 18 | 19 | t.Run("stat", func(t *testing.T) { 20 | ptr, err := fg.GetById("graph/function/graph.go:#160-#166:function||CreateFuncGraphFromDirWithLSIF|string,string,core.LangType|*Graph,error") 21 | assert.Nil(t, err) 22 | stat := fg.GlobalStat([]*Vertex{ptr}) 23 | assert.NotEmpty(t, stat) 24 | assert.NotEmpty(t, stat.ImpactUnitsMap) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /graph/function/api_modify_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/opensibyl/sibyl2/pkg/core" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestModify(t *testing.T) { 13 | _, curFile, _, _ := runtime.Caller(0) 14 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 15 | fg, err := CreateFuncGraphFromDirWithLSIF(src, filepath.Join(src, "dump.lsif"), core.LangGo) 16 | assert.Nil(t, err) 17 | assert.NotEmpty(t, fg.Cache) 18 | 19 | t.Run("RemoveNode", func(t *testing.T) { 20 | before, err := fg.g.Order() 21 | assert.Nil(t, err) 22 | err = fg.RemoveNodeById("graph/function/api_modify_test.go:#12-#28:function||TestModify|*testing.T|") 23 | assert.Nil(t, err) 24 | after, err := fg.g.Order() 25 | assert.Nil(t, err) 26 | assert.Equal(t, before, after+1) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /graph/file/api_draw.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dominikbraun/graph/draw" 7 | ) 8 | 9 | func (fg *Graph) DrawDot(filename string) error { 10 | file, err := os.Create(filename) 11 | if err != nil { 12 | return err 13 | } 14 | // draw the call graph 15 | err = draw.DOT(fg.G, file, draw.GraphAttribute("rankdir", "LR")) 16 | if err != nil { 17 | return err 18 | } 19 | return nil 20 | } 21 | 22 | func (fg *Graph) FillWithRed(vertexHash string) error { 23 | if item, ok := fg.IdCache[vertexHash]; ok { 24 | item.AddTag(TagRed) 25 | } 26 | return nil 27 | } 28 | 29 | func (fg *Graph) setProperty(vertexHash string, propertyK string, propertyV string) error { 30 | _, properties, err := fg.G.VertexWithProperties(vertexHash) 31 | if err != nil { 32 | return err 33 | } 34 | properties.Attributes[propertyK] = propertyV 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /graph/function/api_draw.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | TagYellow = "yellow" 9 | TagRed = "red" 10 | TagOrange = "orange" 11 | ) 12 | 13 | func (fg *Graph) FillWithYellow(vertexHash string) error { 14 | item, ok := fg.IdCache[vertexHash] 15 | if !ok { 16 | return fmt.Errorf("no such vertex: %v", vertexHash) 17 | } 18 | item.AddTag(TagYellow) 19 | return nil 20 | } 21 | 22 | func (fg *Graph) FillWithOrange(vertexHash string) error { 23 | item, ok := fg.IdCache[vertexHash] 24 | if !ok { 25 | return fmt.Errorf("no such vertex: %v", vertexHash) 26 | } 27 | item.AddTag(TagOrange) 28 | return nil 29 | } 30 | 31 | func (fg *Graph) FillWithRed(vertexHash string) error { 32 | item, ok := fg.IdCache[vertexHash] 33 | if !ok { 34 | return fmt.Errorf("no such vertex: %v", vertexHash) 35 | } 36 | item.AddTag(TagRed) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /graph/function/api_storage_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/opensibyl/sibyl2/pkg/core" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestStorage(t *testing.T) { 13 | _, curFile, _, _ := runtime.Caller(0) 14 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 15 | fg, err := CreateFuncGraphFromDirWithLSIF(src, filepath.Join(src, "dump.lsif"), core.LangGo) 16 | assert.Nil(t, err) 17 | assert.NotEmpty(t, fg.Cache) 18 | 19 | temp := "./temp.msgpack" 20 | err = fg.DumpFile(temp) 21 | assert.Nil(t, err) 22 | assert.FileExists(t, temp) 23 | 24 | newFg, err := LoadFile(temp) 25 | assert.Nil(t, err) 26 | 27 | oldOrd, _ := fg.g.Order() 28 | oldSize, _ := fg.g.Size() 29 | newOrd, _ := newFg.g.Order() 30 | newSize, _ := newFg.g.Size() 31 | assert.Equal(t, oldOrd, newOrd) 32 | assert.Equal(t, oldSize, newSize) 33 | } 34 | -------------------------------------------------------------------------------- /graph/function/api_draw_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/opensibyl/sibyl2/pkg/core" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFuncGraph_Draw(t *testing.T) { 14 | _, curFile, _, _ := runtime.Caller(0) 15 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 16 | fg, err := CreateFuncGraphFromDirWithLSIF(src, filepath.Join(src, "dump.lsif"), core.LangGo) 17 | assert.Nil(t, err) 18 | assert.NotEmpty(t, fg.Cache) 19 | 20 | t.Run("DrawDot", func(t *testing.T) { 21 | dotFile := "a.gv" 22 | defer os.Remove(dotFile) 23 | err = fg.DrawDot(dotFile) 24 | assert.Nil(t, err) 25 | assert.FileExists(t, dotFile) 26 | }) 27 | 28 | t.Run("DrawHtml", func(t *testing.T) { 29 | htmlFile := "a.html" 30 | defer os.Remove(htmlFile) 31 | err = fg.DrawG6Html(htmlFile) 32 | assert.Nil(t, err) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /graph/file/graph_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/williamfzc/srctx/graph/common" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGraph(t *testing.T) { 14 | _, curFile, _, _ := runtime.Caller(0) 15 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 16 | 17 | t.Run("from lsif", func(t *testing.T) { 18 | opts := common.DefaultGraphOptions() 19 | opts.Src = src 20 | opts.LsifFile = filepath.Join(src, "dump.lsif") 21 | 22 | fg, err := CreateFileGraphFromDirWithLSIF(opts) 23 | assert.Nil(t, err) 24 | assert.NotEmpty(t, fg) 25 | }) 26 | 27 | t.Run("create index", func(t *testing.T) { 28 | t.Skip("this case did not work in github actions") 29 | opts := common.DefaultGraphOptions() 30 | opts.Src = src 31 | 32 | fg, err := CreateFileGraphFromGolangDir(opts) 33 | assert.Nil(t, err) 34 | assert.NotEmpty(t, fg) 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /parser/lsif/id_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type jsonWithId struct { 11 | Value Id `json:"value"` 12 | } 13 | 14 | func TestId(t *testing.T) { 15 | var v jsonWithId 16 | require.NoError(t, json.Unmarshal([]byte(`{ "value": 1230 }`), &v)) 17 | require.Equal(t, Id(1230), v.Value) 18 | 19 | require.NoError(t, json.Unmarshal([]byte(`{ "value": "1230" }`), &v)) 20 | require.Equal(t, Id(1230), v.Value) 21 | 22 | require.Error(t, json.Unmarshal([]byte(`{ "value": "1.5" }`), &v)) 23 | require.Error(t, json.Unmarshal([]byte(`{ "value": 1.5 }`), &v)) 24 | require.Error(t, json.Unmarshal([]byte(`{ "value": "-1" }`), &v)) 25 | require.Error(t, json.Unmarshal([]byte(`{ "value": -1 }`), &v)) 26 | require.Error(t, json.Unmarshal([]byte(`{ "value": 21000000 }`), &v)) 27 | require.Error(t, json.Unmarshal([]byte(`{ "value": "21000000" }`), &v)) 28 | } 29 | -------------------------------------------------------------------------------- /parser/lsif/hovers_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestHoversRead(t *testing.T) { 10 | h := setupHovers(t) 11 | 12 | var offset Offset 13 | require.NoError(t, h.Offsets.Entry(2, &offset)) 14 | require.Equal(t, Offset{At: 0, Len: 19}, offset) 15 | 16 | require.Equal(t, `[{"value":"hello"}]`, string(h.For(1))) 17 | 18 | require.NoError(t, h.Close()) 19 | } 20 | 21 | func setupHovers(t *testing.T) *Hovers { 22 | h, err := NewHovers() 23 | require.NoError(t, err) 24 | 25 | require.NoError(t, h.Read("hoverResult", []byte(`{"id":"2","label":"hoverResult","result":{"contents": ["hello"]}}`))) 26 | require.NoError(t, h.Read("textDocument/hover", []byte(`{"id":4,"label":"textDocument/hover","outV":"3","inV":2}`))) 27 | require.NoError(t, h.Read("textDocument/references", []byte(`{"id":"3","label":"textDocument/references","outV":3,"inV":"1"}`))) 28 | 29 | return h 30 | } 31 | -------------------------------------------------------------------------------- /matrix/api_test.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | log "github.com/sirupsen/logrus" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/williamfzc/srctx/graph/common" 11 | "github.com/williamfzc/srctx/graph/file" 12 | ) 13 | 14 | func TestMatrix(t *testing.T) { 15 | _, curFile, _, _ := runtime.Caller(0) 16 | src := filepath.Dir(filepath.Dir(curFile)) 17 | opts := common.DefaultGraphOptions() 18 | opts.Src = src 19 | opts.LsifFile = filepath.Join(src, "dump.lsif") 20 | fileGraph, err := file.CreateFileGraphFromDirWithLSIF(opts) 21 | assert.Nil(t, err) 22 | 23 | t.Run("test_a", func(t *testing.T) { 24 | matrixFromGraph, err := CreateMatrixFromGraph(fileGraph.G) 25 | assert.Nil(t, err) 26 | assert.NotEmpty(t, matrixFromGraph) 27 | 28 | targetFile := "graph/common/edge.go" 29 | matrixFromGraph.ForEach(targetFile, func(i int, v float64) { 30 | log.Infof("%s value: %v", matrixFromGraph.ById(i), v) 31 | }) 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /cmd/srctx/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/urfave/cli/v2" 8 | "github.com/williamfzc/srctx" 9 | "github.com/williamfzc/srctx/cmd/srctx/diff" 10 | "github.com/williamfzc/srctx/cmd/srctx/dump" 11 | ) 12 | 13 | func main() { 14 | mainFunc(os.Args) 15 | } 16 | 17 | func mainFunc(args []string) { 18 | app := cli.NewApp() 19 | app.Name = "srctx" 20 | app.Usage = "source context tool" 21 | 22 | diff.AddDiffCmd(app) 23 | diff.AddConfigCmd(app) 24 | dump.AddDumpCmd(app) 25 | 26 | log.Infof("srctx version %v (%s)", srctx.Version, srctx.RepoUrl) 27 | err := app.Run(args) 28 | panicIfErr(err) 29 | } 30 | 31 | func panicIfErr(err error) { 32 | if err != nil { 33 | panic(err) 34 | } 35 | } 36 | 37 | func init() { 38 | environment := os.Getenv("SRCTX_ENV") 39 | 40 | if environment == "production" || environment == "prod" { 41 | // can be saved to log file 42 | log.SetOutput(os.Stdout) 43 | log.SetLevel(log.WarnLevel) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /parser/lsif/id.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "errors" 5 | "strconv" 6 | 7 | "github.com/goccy/go-json" 8 | ) 9 | 10 | const ( 11 | minId = 1 12 | maxId = 20 * 1000 * 1000 13 | ) 14 | 15 | type Id int32 16 | 17 | func (id *Id) UnmarshalJSON(b []byte) error { 18 | if len(b) > 0 && b[0] != '"' { 19 | if err := id.unmarshalInt(b); err != nil { 20 | return err 21 | } 22 | } else { 23 | if err := id.unmarshalString(b); err != nil { 24 | return err 25 | } 26 | } 27 | 28 | if *id < minId || *id > maxId { 29 | return errors.New("json: id is invalid") 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func (id *Id) unmarshalInt(b []byte) error { 36 | return json.Unmarshal(b, (*int32)(id)) 37 | } 38 | 39 | func (id *Id) unmarshalString(b []byte) error { 40 | var s string 41 | if err := json.Unmarshal(b, &s); err != nil { 42 | return err 43 | } 44 | 45 | i, err := strconv.ParseInt(s, 10, 32) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | *id = Id(i) 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /parser/lsif/performance_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func BenchmarkGenerate(b *testing.B) { 14 | filePath := "testdata/workhorse.lsif.zip" 15 | tmpDir := filePath + ".tmp" 16 | defer os.RemoveAll(tmpDir) 17 | 18 | var memoryUsage float64 19 | for i := 0; i < b.N; i++ { 20 | memoryUsage += measureMemory(func() { 21 | file, err := os.Open(filePath) 22 | require.NoError(b, err) 23 | 24 | parser, err := NewParser(context.Background(), file) 25 | require.NoError(b, err) 26 | 27 | _, err = io.Copy(io.Discard, parser) 28 | require.NoError(b, err) 29 | require.NoError(b, parser.Close()) 30 | }) 31 | } 32 | 33 | b.ReportMetric(memoryUsage/float64(b.N), "MiB/op") 34 | } 35 | 36 | func measureMemory(f func()) float64 { 37 | var m, m1 runtime.MemStats 38 | runtime.ReadMemStats(&m) 39 | 40 | f() 41 | 42 | runtime.ReadMemStats(&m1) 43 | runtime.GC() 44 | 45 | return float64(m1.Alloc-m.Alloc) / 1024 / 1024 46 | } 47 | -------------------------------------------------------------------------------- /parser/lsif/references_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | ) 8 | 9 | func TestReferencesStore(t *testing.T) { 10 | const ( 11 | docId = 1 12 | refId = 3 13 | ) 14 | 15 | r, err := NewReferences() 16 | require.NoError(t, err) 17 | 18 | err = r.Store(refId, []Item{{Line: 2, DocId: docId}, {Line: 3, DocId: docId}}) 19 | require.NoError(t, err) 20 | 21 | docs := map[Id]string{docId: "doc.go"} 22 | serializedReferences := r.For(docs, refId) 23 | 24 | require.Contains(t, serializedReferences, SerializedReference{Path: "doc.go#L2"}) 25 | require.Contains(t, serializedReferences, SerializedReference{Path: "doc.go#L3"}) 26 | 27 | require.NoError(t, r.Close()) 28 | } 29 | 30 | func TestReferencesStoreEmpty(t *testing.T) { 31 | const refId = 3 32 | 33 | r, err := NewReferences() 34 | require.NoError(t, err) 35 | 36 | err = r.Store(refId, []Item{}) 37 | require.NoError(t, err) 38 | 39 | docs := map[Id]string{1: "doc.go"} 40 | serializedReferences := r.For(docs, refId) 41 | 42 | require.Nil(t, serializedReferences) 43 | require.NoError(t, r.Close()) 44 | } 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ## Build 4 | FROM golang:1.22-alpine AS build 5 | 6 | WORKDIR /app 7 | 8 | # if you build it in China, add this 9 | #ENV GOPROXY=https://goproxy.cn,direct 10 | 11 | COPY ./go.mod ./ 12 | COPY ./go.sum ./ 13 | COPY ./Makefile ./ 14 | RUN go mod download && \ 15 | apk add --no-cache --update make gcc g++ 16 | 17 | COPY . . 18 | RUN make 19 | 20 | # srctx in /app 21 | 22 | ## Deploy 23 | FROM alpine:3 24 | WORKDIR /srctx_home 25 | # Install git 26 | RUN apk add --no-cache git bash 27 | # lsif-go 28 | RUN wget https://github.com/sourcegraph/lsif-go/releases/download/v1.9.3/src_linux_amd64 -O lsif-go 29 | # golang 30 | COPY --from=golang:1.22-alpine /usr/local/go/ /usr/local/go/ 31 | ENV PATH="/usr/local/go/bin:${PATH}" 32 | 33 | COPY --from=build /app/srctx /srctx_home/srctx 34 | COPY ./scripts/create_and_diff.sh /srctx_home/create_and_diff.sh 35 | 36 | RUN chmod +x /srctx_home/create_and_diff.sh && \ 37 | chmod +x /srctx_home/lsif-go && \ 38 | git config --global --add safe.directory /app 39 | 40 | ENV PATH="${PATH}:/srctx_home" 41 | 42 | ENTRYPOINT ["/srctx_home/create_and_diff.sh"] 43 | -------------------------------------------------------------------------------- /graph/file/api_query.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/williamfzc/srctx/graph/common" 8 | ) 9 | 10 | func (fg *Graph) GetById(id string) *Vertex { 11 | v, err := fg.G.Vertex(id) 12 | if err != nil { 13 | logrus.Warnf("no vertex: %v", id) 14 | return nil 15 | } 16 | return v 17 | } 18 | 19 | func (fg *Graph) ListFiles() []*Vertex { 20 | ret := make([]*Vertex, 0, len(fg.IdCache)) 21 | for _, each := range fg.IdCache { 22 | ret = append(ret, each) 23 | } 24 | return ret 25 | } 26 | 27 | func (fg *Graph) FilterFunctions(f func(*Vertex) bool) []*Vertex { 28 | ret := make([]*Vertex, 0) 29 | for _, each := range fg.IdCache { 30 | if f(each) { 31 | ret = append(ret, each) 32 | } 33 | } 34 | return ret 35 | } 36 | 37 | func (fg *Graph) ListEntries() []*Vertex { 38 | return fg.FilterFunctions(func(vertex *Vertex) bool { 39 | return vertex.ContainTag(TagEntry) 40 | }) 41 | } 42 | 43 | func (fg *Graph) RelationBetween(a string, b string) (*common.EdgeStorage, error) { 44 | edge, err := fg.G.Edge(a, b) 45 | if err != nil { 46 | return nil, err 47 | } 48 | if ret, ok := edge.Properties.Data.(*common.EdgeStorage); ok { 49 | return ret, nil 50 | } 51 | return nil, fmt.Errorf("failed to convert %v", edge.Properties.Data) 52 | } 53 | -------------------------------------------------------------------------------- /example/api_base_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "testing" 5 | 6 | log "github.com/sirupsen/logrus" 7 | "github.com/williamfzc/srctx/parser" 8 | ) 9 | 10 | func TestBase(t *testing.T) { 11 | yourLsif := "../parser/lsif/testdata/dump.lsif.zip" 12 | 13 | sourceContext, err := parser.FromLsifFile(yourLsif, "..") 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | // all files? 19 | files := sourceContext.Files() 20 | log.Infof("files in lsif: %d", len(files)) 21 | 22 | // search definition in a specific file 23 | defs, err := sourceContext.DefsByFileName(files[0]) 24 | if err != nil { 25 | panic(err) 26 | } 27 | log.Infof("there are %d def happend in %s", len(defs), files[0]) 28 | 29 | for _, eachDef := range defs { 30 | log.Infof("happened in %d:%d", eachDef.LineNumber(), eachDef.Range.Character) 31 | } 32 | // or specific line? 33 | _, _ = sourceContext.DefsByLine(files[0], 1) 34 | 35 | // get all the references of a definition 36 | refs, err := sourceContext.RefsFromDefId(defs[0].Id()) 37 | if err != nil { 38 | panic(err) 39 | } 40 | log.Infof("there are %d refs", len(refs)) 41 | for _, eachRef := range refs { 42 | log.Infof("happened in file %s %d:%d", 43 | sourceContext.FileName(eachRef.FileId), 44 | eachRef.LineNumber(), 45 | eachRef.Range.Character) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /graph/function/api_draw_dot.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/dominikbraun/graph/draw" 7 | ) 8 | 9 | func (fg *Graph) setProperty(vertexHash string, propertyK string, propertyV string) error { 10 | _, properties, err := fg.g.VertexWithProperties(vertexHash) 11 | if err != nil { 12 | return err 13 | } 14 | properties.Attributes[propertyK] = propertyV 15 | return nil 16 | } 17 | 18 | func (fg *Graph) DrawDot(filename string) error { 19 | file, err := os.Create(filename) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | for _, each := range fg.ListFunctions() { 25 | if each.ContainTag(TagYellow) { 26 | err := fg.setProperty(each.Id(), "style", "filled") 27 | if err != nil { 28 | return err 29 | } 30 | err = fg.setProperty(each.Id(), "color", "yellow") 31 | if err != nil { 32 | return err 33 | } 34 | } 35 | 36 | if each.ContainTag(TagRed) { 37 | err := fg.setProperty(each.Id(), "style", "filled") 38 | if err != nil { 39 | return err 40 | } 41 | err = fg.setProperty(each.Id(), "color", "red") 42 | if err != nil { 43 | return err 44 | } 45 | } 46 | } 47 | 48 | // draw the call graph 49 | err = draw.DOT(fg.g, file, draw.GraphAttribute("rankdir", "LR")) 50 | if err != nil { 51 | return err 52 | } 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /graph/file/api_query_ctx_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/williamfzc/srctx/graph/common" 9 | 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestQuery(t *testing.T) { 16 | _, curFile, _, _ := runtime.Caller(0) 17 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 18 | 19 | opts := common.DefaultGraphOptions() 20 | opts.Src = src 21 | opts.LsifFile = filepath.Join(src, "dump.lsif") 22 | fg, err := CreateFileGraphFromDirWithLSIF(opts) 23 | assert.Nil(t, err) 24 | assert.NotEmpty(t, fg) 25 | 26 | t.Run("List", func(t *testing.T) { 27 | fs := fg.ListFiles() 28 | assert.NotEmpty(t, fs) 29 | }) 30 | 31 | t.Run("GetHot", func(t *testing.T) { 32 | fileVertex := fg.GetById("graph/function/api_query_test.go") 33 | assert.NotEmpty(t, fileVertex) 34 | 35 | // test file should not be referenced 36 | shouldEmpty := fg.DirectReferencedIds(fileVertex) 37 | assert.Empty(t, shouldEmpty) 38 | 39 | shouldNotEmpty := fg.DirectReferenceIds(fileVertex) 40 | assert.NotEmpty(t, shouldNotEmpty) 41 | log.Infof("outv: %d", len(shouldNotEmpty)) 42 | }) 43 | 44 | t.Run("Entries", func(t *testing.T) { 45 | entries := fg.EntryIds(fg.GetById("graph/function/api_query_test.go")) 46 | // test case usually is an entry point 47 | assert.Len(t, entries, 1) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /graph/function/vertex.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/opensibyl/sibyl2/pkg/extractor" 7 | "github.com/opensibyl/sibyl2/pkg/extractor/object" 8 | ) 9 | 10 | type Vertex struct { 11 | *object.Function 12 | *FuncPos 13 | 14 | // https://github.com/williamfzc/srctx/issues/41 15 | Tags map[string]struct{} `json:"tags,omitempty"` 16 | } 17 | 18 | func (fv *Vertex) Id() string { 19 | return fmt.Sprintf("%v:#%d-#%d:%s", fv.Path, fv.Start, fv.End, fv.GetSignature()) 20 | } 21 | 22 | func (fv *Vertex) PosKey() string { 23 | return fmt.Sprintf("%s#%d", fv.Path, fv.Start) 24 | } 25 | 26 | func (fv *Vertex) ListTags() []string { 27 | ret := make([]string, 0, len(fv.Tags)) 28 | for each := range fv.Tags { 29 | ret = append(ret, each) 30 | } 31 | return ret 32 | } 33 | 34 | func (fv *Vertex) ContainTag(tag string) bool { 35 | if _, ok := fv.Tags[tag]; ok { 36 | return true 37 | } 38 | return false 39 | } 40 | 41 | func (fv *Vertex) AddTag(tag string) { 42 | fv.Tags[tag] = struct{}{} 43 | } 44 | 45 | func (fv *Vertex) RemoveTag(tag string) { 46 | delete(fv.Tags, tag) 47 | } 48 | 49 | func CreateFuncVertex(f *object.Function, fr *extractor.FunctionFileResult) *Vertex { 50 | cur := &Vertex{ 51 | Function: f, 52 | FuncPos: &FuncPos{ 53 | Path: fr.Path, 54 | Lang: string(fr.Language), 55 | // sync with real lines 56 | Start: int(f.GetSpan().Start.Row + 1), 57 | End: int(f.GetSpan().End.Row + 1), 58 | }, 59 | Tags: make(map[string]struct{}), 60 | } 61 | return cur 62 | } 63 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Set up Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: 1.22 24 | 25 | - name: Command Level Test (Golang) 26 | run: | 27 | make 28 | git clone https://github.com/gin-gonic/gin --depth=2 29 | cd gin 30 | ../srctx diff --withIndex --lang GOLANG --outputHtml ../sample.html --outputJson ../sample.json --statJson ../sample_stat.json 31 | cd .. 32 | 33 | - name: Build 34 | uses: crazy-max/ghaction-xgo@v2 35 | with: 36 | xgo_version: latest 37 | go_version: 1.22 38 | pkg: cmd/srctx 39 | dest: build 40 | prefix: srctx 41 | targets: windows/amd64,linux/amd64,linux/arm64,darwin/amd64,darwin/arm64 42 | v: true 43 | x: false 44 | ldflags: -s -w 45 | buildmode: default 46 | 47 | - name: Command Level Test Prepare 48 | run: | 49 | cp build/srctx-linux-amd64 . 50 | chmod +x ./srctx-linux-amd64 51 | 52 | - name: Release 53 | uses: softprops/action-gh-release@v1 54 | with: 55 | files: | 56 | build/srctx-* 57 | sample.html 58 | sample.json 59 | sample_stat.json 60 | -------------------------------------------------------------------------------- /example/api_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/opensibyl/sibyl2/pkg/core" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/williamfzc/srctx/graph/function" 11 | ) 12 | 13 | func TestFunc(t *testing.T) { 14 | _, curFile, _, _ := runtime.Caller(0) 15 | src := filepath.Dir(filepath.Dir(curFile)) 16 | lsif := "../dump.lsif" 17 | lang := core.LangGo 18 | 19 | funcGraph, err := function.CreateFuncGraphFromDirWithLSIF(src, lsif, lang) 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | t.Run("file", func(t *testing.T) { 25 | files := funcGraph.ListFiles() 26 | for _, each := range files { 27 | log.Debugf("file: %v", each) 28 | } 29 | }) 30 | 31 | t.Run("func", func(t *testing.T) { 32 | functions := funcGraph.GetFunctionsByFile("graph/function/api_query.go") 33 | for _, each := range functions { 34 | // about this function 35 | log.Infof("func: %v", each.Id()) 36 | log.Infof("decl location: %v", each.FuncPos.Repr()) 37 | log.Infof("func name: %v", each.Name) 38 | 39 | // context of this function 40 | outVs := funcGraph.DirectReferencedIds(each) 41 | log.Infof("this function referenced by %v other functions", len(outVs)) 42 | for _, eachOutV := range outVs { 43 | outV, _ := funcGraph.GetById(eachOutV) 44 | log.Infof("%v directly referenced by %v", each.Name, outV.Name) 45 | } 46 | transOutVs := funcGraph.TransitiveReferencedIds(each) 47 | log.Infof("this function transitively referenced by %d other functions", len(transOutVs)) 48 | 49 | allEntries := funcGraph.ListEntries() 50 | entries := funcGraph.EntryIds(each) 51 | log.Infof("this function affects %d/%d entries", len(entries), len(allEntries)) 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /graph/file/api_transform.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/dominikbraun/graph" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func path2dir(fp string) string { 11 | return filepath.ToSlash(filepath.Dir(fp)) 12 | } 13 | 14 | func Path2vertex(fp string) *Vertex { 15 | return &Vertex{ 16 | Path: fp, 17 | Tags: make(map[string]struct{}), 18 | } 19 | } 20 | 21 | func fileGraph2FileGraph(f graph.Graph[string, *Vertex], g graph.Graph[string, *Vertex]) error { 22 | m, err := f.AdjacencyMap() 23 | if err != nil { 24 | return err 25 | } 26 | // add all the vertices 27 | for k := range m { 28 | _ = g.AddVertex(Path2vertex(path2dir(k))) 29 | } 30 | 31 | edges, err := f.Edges() 32 | if err != nil { 33 | return err 34 | } 35 | for _, eachEdge := range edges { 36 | source, err := f.Vertex(eachEdge.Source) 37 | if err != nil { 38 | log.Warnf("vertex not found: %v", eachEdge.Source) 39 | continue 40 | } 41 | dirSourcePath := path2dir(source.Path) 42 | 43 | target, err := f.Vertex(eachEdge.Target) 44 | if err != nil { 45 | log.Warnf("vertex not found: %v", eachEdge.Target) 46 | continue 47 | } 48 | dirTargetPath := path2dir(target.Path) 49 | 50 | // ignore self ptr 51 | if dirSourcePath == dirTargetPath { 52 | continue 53 | } 54 | 55 | sourceFile, err := g.Vertex(dirSourcePath) 56 | if err != nil { 57 | return err 58 | } 59 | targetFile, err := g.Vertex(dirTargetPath) 60 | if err != nil { 61 | return err 62 | } 63 | if sv, err := g.Vertex(sourceFile.Id()); err == nil { 64 | sv.Referenced++ 65 | } else { 66 | _ = g.AddVertex(sourceFile) 67 | } 68 | if tv, err := g.Vertex(targetFile.Id()); err == nil { 69 | tv.Referenced++ 70 | } else { 71 | _ = g.AddVertex(targetFile) 72 | } 73 | 74 | _ = g.AddEdge(sourceFile.Id(), targetFile.Id()) 75 | } 76 | 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /graph/function/api_query.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/williamfzc/srctx/graph/common" 7 | ) 8 | 9 | func (fg *Graph) GetFunctionsByFile(fileName string) []*Vertex { 10 | if item, ok := fg.Cache[fileName]; ok { 11 | return item 12 | } 13 | return make([]*Vertex, 0) 14 | } 15 | 16 | func (fg *Graph) GetFunctionsByFileLines(fileName string, lines []int) []*Vertex { 17 | ret := make([]*Vertex, 0) 18 | functions := fg.Cache[fileName] 19 | if len(functions) == 0 { 20 | return ret 21 | } 22 | 23 | for _, eachFunc := range functions { 24 | // append these def lines 25 | if eachFunc.GetSpan().ContainAnyLine(lines...) { 26 | ret = append(ret, eachFunc) 27 | } 28 | } 29 | return ret 30 | } 31 | 32 | func (fg *Graph) GetById(id string) (*Vertex, error) { 33 | if item, ok := fg.IdCache[id]; ok { 34 | return item, nil 35 | } 36 | return nil, fmt.Errorf("id not found in graph: %s", id) 37 | } 38 | 39 | func (fg *Graph) FuncCount() int { 40 | return len(fg.IdCache) 41 | } 42 | 43 | func (fg *Graph) ListFunctions() []*Vertex { 44 | return fg.FilterFunctions(func(funcVertex *Vertex) bool { 45 | return true 46 | }) 47 | } 48 | 49 | func (fg *Graph) FilterFunctions(f func(*Vertex) bool) []*Vertex { 50 | ret := make([]*Vertex, 0) 51 | for _, each := range fg.IdCache { 52 | if f(each) { 53 | ret = append(ret, each) 54 | } 55 | } 56 | return ret 57 | } 58 | 59 | func (fg *Graph) ListEntries() []*Vertex { 60 | return fg.FilterFunctions(func(funcVertex *Vertex) bool { 61 | return funcVertex.ContainTag(TagEntry) 62 | }) 63 | } 64 | 65 | func (fg *Graph) RelationBetween(a string, b string) (*common.EdgeStorage, error) { 66 | edge, err := fg.g.Edge(a, b) 67 | if err != nil { 68 | return nil, err 69 | } 70 | if ret, ok := edge.Properties.Data.(*common.EdgeStorage); ok { 71 | return ret, nil 72 | } 73 | return nil, fmt.Errorf("failed to convert %v", edge.Properties.Data) 74 | } 75 | -------------------------------------------------------------------------------- /graph/file/api_test.go: -------------------------------------------------------------------------------- 1 | package file_test 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/williamfzc/srctx/graph/common" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/williamfzc/srctx/graph/file" 13 | ) 14 | 15 | func TestFileGraph(t *testing.T) { 16 | _, curFile, _, _ := runtime.Caller(0) 17 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 18 | opts := common.DefaultGraphOptions() 19 | opts.Src = src 20 | opts.LsifFile = filepath.Join(src, "dump.lsif") 21 | fileGraph, err := file.CreateFileGraphFromDirWithLSIF(opts) 22 | assert.Nil(t, err) 23 | 24 | t.Run("Transform", func(t *testing.T) { 25 | size, err := fileGraph.G.Size() 26 | assert.Nil(t, err) 27 | assert.NotEqual(t, size, 0) 28 | 29 | // dir level 30 | dirGraph, err := fileGraph.ToDirGraph() 31 | assert.Nil(t, err) 32 | assert.NotEqual(t, dirGraph, 0) 33 | }) 34 | 35 | t.Run("RemoveNode", func(t *testing.T) { 36 | before, err := fileGraph.G.Order() 37 | assert.Nil(t, err) 38 | err = fileGraph.RemoveNodeById("graph/file/api_test.go") 39 | assert.Nil(t, err) 40 | after, err := fileGraph.G.Order() 41 | assert.Nil(t, err) 42 | assert.Equal(t, before, after+1) 43 | }) 44 | 45 | t.Run("DrawG6", func(t *testing.T) { 46 | err = fileGraph.DrawG6Html("b.html") 47 | assert.Nil(t, err) 48 | }) 49 | 50 | t.Run("Relation", func(t *testing.T) { 51 | edgeStorage, err := fileGraph.RelationBetween("graph/function/api_query.go", "graph/function/api_query_test.go") 52 | assert.Nil(t, err) 53 | log.Debugf("ref lines: %v", edgeStorage.RefLines) 54 | assert.NotEmpty(t, edgeStorage.RefLines) 55 | }) 56 | 57 | t.Run("stat", func(t *testing.T) { 58 | ptr := fileGraph.GetById("graph/function/api_query.go") 59 | stat := fileGraph.GlobalStat([]*file.Vertex{ptr}) 60 | assert.NotEmpty(t, stat) 61 | assert.NotEmpty(t, stat.ImpactUnitsMap) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /object/ctx_fact.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dominikbraun/graph" 7 | ) 8 | 9 | func (sc *SourceContext) Files() []string { 10 | ret := make([]string, 0, len(sc.FileMapping)) 11 | for k := range sc.FileMapping { 12 | ret = append(ret, k) 13 | } 14 | return ret 15 | } 16 | 17 | func (sc *SourceContext) FileId(fileName string) int { 18 | return sc.FileMapping[fileName] 19 | } 20 | 21 | func (sc *SourceContext) FileName(fileId int) string { 22 | for curName, curFileId := range sc.FileMapping { 23 | if curFileId == fileId { 24 | return curName 25 | } 26 | } 27 | return "" 28 | } 29 | 30 | func (sc *SourceContext) FileVertexByName(fileName string) *FactVertex { 31 | factVertex, err := sc.FactGraph.Vertex(sc.FileId(fileName)) 32 | if err != nil { 33 | // if not found 34 | return nil 35 | } 36 | return factVertex 37 | } 38 | 39 | func (sc *SourceContext) DefsByFileName(fileName string) ([]*FactVertex, error) { 40 | startId := sc.FileId(fileName) 41 | if startId == 0 { 42 | return nil, fmt.Errorf("no file named: %s", fileName) 43 | } 44 | 45 | ret := make([]*FactVertex, 0) 46 | err := graph.DFS(sc.FactGraph, startId, func(i int) bool { 47 | // exclude itself 48 | if i == startId { 49 | return false 50 | } 51 | 52 | vertex, err := sc.FactGraph.Vertex(i) 53 | if err != nil { 54 | return true 55 | } 56 | 57 | ret = append(ret, vertex) 58 | return false 59 | }) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return ret, nil 64 | } 65 | 66 | func (sc *SourceContext) DefsByLine(fileName string, lineNum int) ([]*FactVertex, error) { 67 | allVertexes, err := sc.DefsByFileName(fileName) 68 | if err != nil { 69 | return nil, err 70 | } 71 | final := make([]*FactVertex, 0) 72 | for _, each := range allVertexes { 73 | if each.LineNumber() == lineNum { 74 | final = append(final, each) 75 | } 76 | } 77 | if len(final) == 0 { 78 | return nil, fmt.Errorf("no def found in %s %d", fileName, lineNum) 79 | } 80 | 81 | return final, nil 82 | } 83 | -------------------------------------------------------------------------------- /graph/function/api_query_test.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | log "github.com/sirupsen/logrus" 9 | 10 | "github.com/opensibyl/sibyl2/pkg/core" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestQuery(t *testing.T) { 15 | _, curFile, _, _ := runtime.Caller(0) 16 | src := filepath.Dir(filepath.Dir(filepath.Dir(curFile))) 17 | fg, err := CreateFuncGraphFromDirWithLSIF(src, filepath.Join(src, "dump.lsif"), core.LangGo) 18 | assert.Nil(t, err) 19 | assert.NotEmpty(t, fg.Cache) 20 | 21 | t.Run("GetFunctionsByFile", func(t *testing.T) { 22 | testFuncs := fg.GetFunctionsByFile("graph/function/api_query_test.go") 23 | assert.NotEmpty(t, testFuncs) 24 | 25 | for _, eachFunc := range testFuncs { 26 | if eachFunc.Name == "TestQuery" { 27 | beingRefs := fg.DirectReferencedIds(eachFunc) 28 | refOut := fg.DirectReferenceIds(eachFunc) 29 | assert.Len(t, beingRefs, 0) 30 | assert.Len(t, refOut, 9) 31 | } 32 | } 33 | }) 34 | 35 | t.Run("Entries", func(t *testing.T) { 36 | testFuncs := fg.GetFunctionsByFile("graph/function/api_query_test.go") 37 | assert.NotEmpty(t, testFuncs) 38 | 39 | entries := fg.ListEntries() 40 | assert.NotEmpty(t, entries) 41 | 42 | for _, eachFunc := range testFuncs { 43 | if eachFunc.Name == "TestQuery" { 44 | entries := fg.EntryIds(eachFunc) 45 | // test case usually is an entry point 46 | assert.Len(t, entries, 1) 47 | } 48 | } 49 | }) 50 | 51 | t.Run("Relation", func(t *testing.T) { 52 | testFuncs := fg.GetFunctionsByFile("graph/function/api_query_test.go") 53 | assert.NotEmpty(t, testFuncs) 54 | 55 | function, err := fg.GetById("graph/function/api_query.go:#9-#14:function|*Graph|GetFunctionsByFile|string|") 56 | assert.Nil(t, err) 57 | assert.NotNil(t, function) 58 | 59 | edgeStorage, err := fg.RelationBetween(function.Id(), testFuncs[0].Id()) 60 | assert.Nil(t, err) 61 | log.Debugf("ref lines: %v", edgeStorage.RefLines) 62 | assert.NotEmpty(t, edgeStorage.RefLines) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /parser/api_test.go: -------------------------------------------------------------------------------- 1 | package parser 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/dominikbraun/graph" 9 | log "github.com/sirupsen/logrus" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestApi(t *testing.T) { 14 | UseTempFileCache() 15 | srcctxResult, err := FromLsifFile("./lsif/testdata/dump.lsif.zip", ".") 16 | assert.Nil(t, err) 17 | 18 | factGraph := srcctxResult.FactGraph 19 | relGraph := srcctxResult.RelGraph 20 | 21 | t.Run("test_internal", func(t *testing.T) { 22 | // test these graphs 23 | _ = graph.DFS(factGraph, 4, func(i int) bool { 24 | vertex, err := factGraph.Vertex(i) 25 | if err != nil { 26 | return true 27 | } 28 | log.Infof("def in file %d range: %v", vertex.FileId, vertex.Range) 29 | 30 | // any links? 31 | relVertex, err := relGraph.Vertex(i) 32 | if err != nil { 33 | return false 34 | } 35 | err = graph.BFS(relGraph, relVertex.Id(), func(j int) bool { 36 | cur, err := factGraph.Vertex(j) 37 | if err != nil { 38 | return true 39 | } 40 | log.Infof("refered by file %d range: %v", cur.FileId, cur.Range) 41 | return false 42 | }) 43 | if err != nil { 44 | return false 45 | } 46 | 47 | return false 48 | }) 49 | }) 50 | 51 | t.Run("test_ctx", func(t *testing.T) { 52 | fileName := "morestrings/reverse.go" 53 | allDefVertexes, err := srcctxResult.DefsByFileName(fileName) 54 | assert.Nil(t, err) 55 | for _, each := range allDefVertexes { 56 | vertices, err := srcctxResult.RefsFromDefId(each.Id()) 57 | assert.Nil(t, err) 58 | for _, eachV := range vertices { 59 | log.Infof("def in file %s %d:%d, ref in: %s %d:%d", 60 | fileName, each.LineNumber(), 61 | each.Range.Character+1, 62 | srcctxResult.FileName(eachV.FileId), 63 | eachV.LineNumber(), 64 | eachV.Range.Character+1) 65 | } 66 | } 67 | }) 68 | } 69 | 70 | func TestGen(t *testing.T) { 71 | UseMemCache() 72 | _, curFile, _, _ := runtime.Caller(0) 73 | root := filepath.Dir(filepath.Dir(curFile)) 74 | _, err := FromGolangSrc(root) 75 | assert.Nil(t, err) 76 | } 77 | -------------------------------------------------------------------------------- /object/impact.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | type FileInfoPart struct { 4 | FileName string `csv:"fileName" json:"fileName"` 5 | 6 | // actually graph will not access the real file system 7 | // so of course it knows nothing about the real files 8 | // all the data we can access is from the indexing file 9 | } 10 | 11 | type UnitImpactPart struct { 12 | // unit level 13 | // if file level, UnitName == FileName 14 | // if func level, UnitName == FuncSignature 15 | // ... 16 | UnitName string `csv:"unitName" json:"unitName"` 17 | 18 | // Heat 19 | ImpactCount int `csv:"impactCount" json:"impactCount"` 20 | TransImpactCount int `csv:"transImpactCount" json:"transImpactCount"` 21 | 22 | // entries 23 | ImpactEntries int `csv:"impactEntries" json:"impactEntries"` 24 | } 25 | 26 | type GraphVertex interface { 27 | Id() string 28 | ContainTag(tag string) bool 29 | } 30 | 31 | type ImpactDetails struct { 32 | // raw 33 | Self GraphVertex `json:"-" csv:"-"` 34 | ReferencedIds []string `json:"-" csv:"-"` 35 | ReferenceIds []string `json:"-" csv:"-"` 36 | TransitiveReferencedIds []string `json:"-" csv:"-"` 37 | TransitiveReferenceIds []string `json:"-" csv:"-"` 38 | Entries []string `json:"-" csv:"-"` 39 | } 40 | 41 | type ImpactUnit struct { 42 | *FileInfoPart 43 | *UnitImpactPart 44 | *ImpactDetails `json:"-" csv:"-"` 45 | } 46 | 47 | func NewImpactUnit() *ImpactUnit { 48 | return &ImpactUnit{ 49 | FileInfoPart: &FileInfoPart{ 50 | FileName: "", 51 | }, 52 | UnitImpactPart: &UnitImpactPart{ 53 | UnitName: "", 54 | ImpactCount: 0, 55 | TransImpactCount: 0, 56 | ImpactEntries: 0, 57 | }, 58 | ImpactDetails: &ImpactDetails{ 59 | Self: nil, 60 | ReferencedIds: make([]string, 0), 61 | ReferenceIds: make([]string, 0), 62 | TransitiveReferencedIds: make([]string, 0), 63 | TransitiveReferenceIds: make([]string, 0), 64 | }, 65 | } 66 | } 67 | 68 | func (iu *ImpactUnit) VisitedIds() []string { 69 | return append(iu.TransitiveReferenceIds, iu.TransitiveReferencedIds...) 70 | } 71 | -------------------------------------------------------------------------------- /graph/file/api_query_ctx.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/dominikbraun/graph" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func (fg *Graph) DirectReferencedCount(f *Vertex) int { 9 | return len(fg.DirectReferencedIds(f)) 10 | } 11 | 12 | func (fg *Graph) DirectReferencedIds(f *Vertex) []string { 13 | adjacencyMap, err := fg.G.AdjacencyMap() 14 | if err != nil { 15 | log.Warnf("failed to get adjacency map: %v", f) 16 | return nil 17 | } 18 | m := adjacencyMap[f.Id()] 19 | ret := make([]string, 0, len(m)) 20 | for k := range m { 21 | ret = append(ret, k) 22 | } 23 | return ret 24 | } 25 | 26 | func (fg *Graph) DirectReferenceIds(f *Vertex) []string { 27 | adjacencyMap, err := fg.Rg.AdjacencyMap() 28 | if err != nil { 29 | log.Warnf("failed to get adjacency map: %v", f) 30 | return nil 31 | } 32 | m := adjacencyMap[f.Id()] 33 | ret := make([]string, 0, len(m)) 34 | for k := range m { 35 | ret = append(ret, k) 36 | } 37 | return ret 38 | } 39 | 40 | func (fg *Graph) TransitiveReferencedIds(f *Vertex) []string { 41 | m := make(map[string]struct{}, 0) 42 | start := f.Id() 43 | graph.BFS(fg.G, start, func(cur string) bool { 44 | if cur == start { 45 | return false 46 | } 47 | m[cur] = struct{}{} 48 | return false 49 | }) 50 | ret := make([]string, 0, len(m)) 51 | for k := range m { 52 | ret = append(ret, k) 53 | } 54 | return ret 55 | } 56 | 57 | func (fg *Graph) TransitiveReferenceIds(f *Vertex) []string { 58 | m := make(map[string]struct{}, 0) 59 | start := f.Id() 60 | graph.BFS(fg.Rg, start, func(cur string) bool { 61 | if cur == start { 62 | return false 63 | } 64 | m[cur] = struct{}{} 65 | return false 66 | }) 67 | ret := make([]string, 0, len(m)) 68 | for k := range m { 69 | ret = append(ret, k) 70 | } 71 | return ret 72 | } 73 | 74 | func (fg *Graph) EntryIds(f *Vertex) []string { 75 | ret := make([]string, 0) 76 | // and also itself 77 | all := append(fg.TransitiveReferencedIds(f), f.Id()) 78 | for _, eachId := range all { 79 | item := fg.IdCache[eachId] 80 | if item.ContainTag(TagEntry) { 81 | ret = append(ret, eachId) 82 | } 83 | } 84 | return ret 85 | } 86 | -------------------------------------------------------------------------------- /parser/lsif/ranges_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRangesRead(t *testing.T) { 11 | r, cleanup := setup(t) 12 | defer cleanup() 13 | 14 | firstRange := Range{Line: 1, Character: 2, RefId: 4} 15 | rg, err := r.getRange(1) 16 | require.NoError(t, err) 17 | require.Equal(t, &firstRange, rg) 18 | 19 | secondRange := Range{Line: 5, Character: 4, RefId: 4} 20 | rg, err = r.getRange(2) 21 | require.NoError(t, err) 22 | require.Equal(t, &secondRange, rg) 23 | 24 | thirdRange := Range{Line: 7, Character: 4, RefId: 4} 25 | rg, err = r.getRange(3) 26 | require.NoError(t, err) 27 | require.Equal(t, &thirdRange, rg) 28 | } 29 | 30 | func TestSerialize(t *testing.T) { 31 | r, cleanup := setup(t) 32 | defer cleanup() 33 | 34 | docs := map[Id]string{6: "def-path", 7: "ref-path"} 35 | 36 | var buf bytes.Buffer 37 | err := r.Serialize(&buf, []Id{1}, docs) 38 | want := `[{"start_line":1,"start_char":2,"definition_path":"def-path#L2","hover":null,"references":[{"path":"ref-path#L6"},{"path":"ref-path#L8"}]}` + "\n]" 39 | 40 | require.NoError(t, err) 41 | require.Equal(t, want, buf.String()) 42 | } 43 | 44 | func setup(t *testing.T) (*Ranges, func()) { 45 | r, err := NewRanges() 46 | require.NoError(t, err) 47 | 48 | require.NoError(t, r.Read("range", []byte(`{"id":1,"label":"range","start":{"line":1,"character":2}}`))) 49 | require.NoError(t, r.Read("range", []byte(`{"id":"2","label":"range","start":{"line":5,"character":4}}`))) 50 | require.NoError(t, r.Read("range", []byte(`{"id":"3","label":"range","start":{"line":7,"character":4}}`))) 51 | 52 | require.NoError(t, r.Read("item", []byte(`{"id":5,"label":"item","property":"definitions","outV":"4","inVs":[1],"document":"6"}`))) 53 | require.NoError(t, r.Read("item", []byte(`{"id":"6","label":"item","property":"references","outV":4,"inVs":["2"],"document":"7"}`))) 54 | require.NoError(t, r.Read("item", []byte(`{"id":"7","label":"item","property":"references","outV":4,"inVs":["3"],"document":"7"}`))) 55 | 56 | cleanup := func() { 57 | require.NoError(t, r.Close()) 58 | } 59 | 60 | return r, cleanup 61 | } 62 | -------------------------------------------------------------------------------- /graph/function/api_draw_g6.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | "strconv" 6 | 7 | "github.com/williamfzc/srctx/graph/visual/g6" 8 | ) 9 | 10 | func (fg *Graph) ToG6Data() (*g6.Data, error) { 11 | storage, err := fg.Dump() 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | data := g6.EmptyG6Data() 17 | // cache 18 | cache := make(map[string]*Vertex) 19 | // dir combos (#35) 20 | dirCombos := make(map[string]struct{}) 21 | for eachFile, fs := range fg.Cache { 22 | for _, eachF := range fs { 23 | cache[eachF.Id()] = eachF 24 | } 25 | 26 | eachDir := filepath.Dir(eachFile) 27 | dirCombos[eachDir] = struct{}{} 28 | data.Combos = append(data.Combos, &g6.Combo{ 29 | Id: eachFile, 30 | Label: eachFile, 31 | Collapsed: false, 32 | ParentId: eachDir, 33 | }) 34 | } 35 | 36 | for eachDir := range dirCombos { 37 | data.Combos = append(data.Combos, &g6.Combo{ 38 | Id: eachDir, 39 | Label: eachDir, 40 | Collapsed: false, 41 | }) 42 | } 43 | 44 | // Nodes 45 | for nodeId, funcId := range storage.VertexIds { 46 | funcObj := cache[funcId] 47 | curNode := &g6.Node{ 48 | Id: strconv.Itoa(nodeId), 49 | Label: funcId, 50 | Style: &g6.NodeStyle{}, 51 | ComboId: funcObj.Path, 52 | } 53 | 54 | if funcObj.ContainTag(TagYellow) { 55 | curNode.Style.Fill = "yellow" 56 | } 57 | if funcObj.ContainTag(TagOrange) { 58 | curNode.Style.Fill = "orange" 59 | } 60 | if funcObj.ContainTag(TagRed) { 61 | curNode.Style.Fill = "red" 62 | } 63 | 64 | data.Nodes = append(data.Nodes, curNode) 65 | } 66 | // Edges 67 | for src, targets := range storage.GEdges { 68 | for _, target := range targets { 69 | curEdge := &g6.Edge{ 70 | Source: strconv.Itoa(src), 71 | Target: strconv.Itoa(target), 72 | } 73 | data.Edges = append(data.Edges, curEdge) 74 | } 75 | } 76 | return data, nil 77 | } 78 | 79 | func (fg *Graph) DrawG6Html(filename string) error { 80 | g6data, err := fg.ToG6Data() 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = g6data.RenderHtml(filename) 86 | if err != nil { 87 | return err 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /diff/git.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | "path/filepath" 7 | 8 | "github.com/bluekeyes/go-gitdiff/gitdiff" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // ImpactLineMap file name -> lines 13 | type ImpactLineMap = map[string][]int 14 | 15 | func GitDiff(rootDir string, before string, after string) (ImpactLineMap, error) { 16 | // about why, I use cmd rather than some libs 17 | // because go-git 's patch has some bugs ... 18 | gitDiffCmd := exec.Command("git", "diff", before, after) 19 | gitDiffCmd.Dir = rootDir 20 | data, err := gitDiffCmd.CombinedOutput() 21 | if err != nil { 22 | log.Errorf("git cmd error: %s", data) 23 | return nil, err 24 | } 25 | 26 | affected, err := Unified2Impact(data) 27 | if err != nil { 28 | return nil, err 29 | } 30 | return affected, nil 31 | } 32 | 33 | func PathOffset(repoRoot string, srcRoot string, origin ImpactLineMap) (ImpactLineMap, error) { 34 | modifiedLineMap := make(map[string][]int) 35 | for file, lines := range origin { 36 | afterPath, err := PathOffsetOne(repoRoot, srcRoot, file) 37 | if err != nil { 38 | return nil, err 39 | } 40 | modifiedLineMap[afterPath] = lines 41 | } 42 | return modifiedLineMap, nil 43 | } 44 | 45 | func PathOffsetOne(repoRoot string, srcRoot string, target string) (string, error) { 46 | absFile := filepath.Join(repoRoot, target) 47 | return filepath.Rel(srcRoot, absFile) 48 | } 49 | 50 | func Unified2Impact(patch []byte) (ImpactLineMap, error) { 51 | parsed, _, err := gitdiff.Parse(bytes.NewReader(patch)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | impactLineMap := make(ImpactLineMap) 57 | for _, each := range parsed { 58 | if each.IsBinary || each.IsDelete { 59 | continue 60 | } 61 | impactLineMap[each.NewName] = make([]int, 0) 62 | fragments := each.TextFragments 63 | for _, eachFragment := range fragments { 64 | left := int(eachFragment.NewPosition) 65 | 66 | for i, eachLine := range eachFragment.Lines { 67 | if eachLine.New() && eachLine.Op == gitdiff.OpAdd { 68 | impactLineMap[each.NewName] = append(impactLineMap[each.NewName], left+i-1) 69 | } 70 | } 71 | } 72 | } 73 | return impactLineMap, nil 74 | } 75 | -------------------------------------------------------------------------------- /graph/file/api_draw_g6.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "path/filepath" 5 | "strconv" 6 | 7 | "github.com/williamfzc/srctx/graph/visual/g6" 8 | ) 9 | 10 | const TagRed = "red" 11 | 12 | func (fg *Graph) ToG6Data() (*g6.Data, error) { 13 | data := g6.EmptyG6Data() 14 | 15 | adjacencyMap, err := fg.G.AdjacencyMap() 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | // cache 21 | cache := make(map[string]*Vertex) 22 | // dir combos 23 | dirCombos := make(map[string]struct{}) 24 | for nodeId := range adjacencyMap { 25 | node, err := fg.G.Vertex(nodeId) 26 | if err != nil { 27 | return nil, err 28 | } 29 | cache[node.Id()] = node 30 | 31 | eachDir := filepath.Dir(node.Path) 32 | dirCombos[eachDir] = struct{}{} 33 | data.Combos = append(data.Combos, &g6.Combo{ 34 | Id: eachDir, 35 | Label: eachDir, 36 | Collapsed: false, 37 | }) 38 | } 39 | 40 | // mapping 41 | mapping := make(map[string]int) 42 | curId := 0 43 | 44 | // Nodes 45 | for nodeId := range adjacencyMap { 46 | node, err := fg.G.Vertex(nodeId) 47 | if err != nil { 48 | return nil, err 49 | } 50 | mapping[node.Id()] = curId 51 | curNode := &g6.Node{ 52 | Id: strconv.Itoa(curId), 53 | Label: node.Id(), 54 | Style: &g6.NodeStyle{}, 55 | ComboId: filepath.Dir(node.Path), 56 | } 57 | if node.ContainTag(TagRed) { 58 | curNode.Style.Fill = "red" 59 | } 60 | 61 | curId++ 62 | data.Nodes = append(data.Nodes, curNode) 63 | } 64 | 65 | // Edges 66 | for src, targets := range adjacencyMap { 67 | for target := range targets { 68 | srcNode := cache[src] 69 | targetNode := cache[target] 70 | 71 | srcId := mapping[srcNode.Id()] 72 | targetId := mapping[targetNode.Id()] 73 | 74 | curEdge := &g6.Edge{ 75 | Source: strconv.Itoa(srcId), 76 | Target: strconv.Itoa(targetId), 77 | } 78 | data.Edges = append(data.Edges, curEdge) 79 | } 80 | } 81 | 82 | return data, nil 83 | } 84 | 85 | func (fg *Graph) DrawG6Html(filename string) error { 86 | data, err := fg.ToG6Data() 87 | if err != nil { 88 | return err 89 | } 90 | err = data.RenderHtml(filename) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /graph/function/fact.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/opensibyl/sibyl2" 7 | "github.com/opensibyl/sibyl2/pkg/core" 8 | "github.com/opensibyl/sibyl2/pkg/extractor" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func CreateFact(root string, lang core.LangType) (*FactStorage, error) { 13 | abs, err := filepath.Abs(root) 14 | if err != nil { 15 | return nil, err 16 | } 17 | conf := sibyl2.DefaultConfig() 18 | conf.LangType = lang 19 | functionFiles, err := sibyl2.ExtractFunction(abs, conf) 20 | if err != nil { 21 | return nil, err 22 | } 23 | symbolFiles, err := sibyl2.ExtractSymbol(abs, conf) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | fact := &FactStorage{ 29 | cache: make(map[string]*extractor.FunctionFileResult, len(functionFiles)), 30 | symbolCache: make(map[string]*extractor.SymbolFileResult, len(symbolFiles)), 31 | } 32 | for _, eachFunc := range functionFiles { 33 | log.Debugf("create func file for: %v", eachFunc.Path) 34 | fact.cache[eachFunc.Path] = eachFunc 35 | } 36 | for _, eachSymbol := range symbolFiles { 37 | log.Debugf("create symbol file for: %v", eachSymbol.Path) 38 | fact.symbolCache[eachSymbol.Path] = eachSymbol 39 | } 40 | 41 | return fact, nil 42 | } 43 | 44 | // FactStorage 45 | // fact is some extra metadata extracted from source code 46 | // something like: function definitions with their annotations/params/receiver ... 47 | // these data can be used for enhancing relationship 48 | type FactStorage struct { 49 | cache map[string]*extractor.FunctionFileResult 50 | symbolCache map[string]*extractor.SymbolFileResult 51 | } 52 | 53 | func (fs *FactStorage) GetFunctionsByFile(fileName string) *extractor.FunctionFileResult { 54 | return fs.cache[fileName] 55 | } 56 | 57 | func (fs *FactStorage) GetSymbolsByFileAndLine(fileName string, line int) []*extractor.Symbol { 58 | item, ok := fs.symbolCache[fileName] 59 | if !ok { 60 | log.Warnf("failed to get symbol: %v", fileName) 61 | return nil 62 | } 63 | ret := make([]*extractor.Symbol, 0) 64 | for _, eachUnit := range item.Units { 65 | if eachUnit.GetSpan().ContainLine(line - 1) { 66 | ret = append(ret, eachUnit) 67 | } 68 | } 69 | return ret 70 | } 71 | -------------------------------------------------------------------------------- /parser/lsif/docs_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func createLine(id, label, uri string) []byte { 13 | return []byte(fmt.Sprintf(`{"id":"%s","label":"%s","uri":"%s"}`+"\n", id, label, uri)) 14 | } 15 | 16 | func TestParse(t *testing.T) { 17 | d, err := NewDocs() 18 | require.NoError(t, err) 19 | defer d.Close() 20 | 21 | for _, root := range []string{ 22 | "file:///Users/nested", 23 | "file:///Users/nested/.", 24 | "file:///Users/nested/", 25 | } { 26 | t.Run("Document with root: "+root, func(t *testing.T) { 27 | data := []byte(`{"id":"1","label":"metaData","projectRoot":"` + root + `"}` + "\n") 28 | data = append(data, createLine("2", "document", "file:///Users/nested/file.rb")...) 29 | data = append(data, createLine("3", "document", "file:///Users/nested/folder/file.rb")...) 30 | 31 | require.NoError(t, d.Parse(bytes.NewReader(data))) 32 | 33 | require.Equal(t, "file.rb", d.Entries[2]) 34 | require.Equal(t, "folder/file.rb", d.Entries[3]) 35 | }) 36 | } 37 | 38 | t.Run("Relative path cannot be calculated", func(t *testing.T) { 39 | originalUri := "file:///Users/nested/folder/file.rb" 40 | data := []byte(`{"id":"1","label":"metaData","projectRoot":"/a"}` + "\n") 41 | data = append(data, createLine("2", "document", originalUri)...) 42 | 43 | require.NoError(t, d.Parse(bytes.NewReader(data))) 44 | 45 | require.Equal(t, originalUri, d.Entries[2]) 46 | }) 47 | } 48 | 49 | func TestParseContainsLine(t *testing.T) { 50 | d, err := NewDocs() 51 | require.NoError(t, err) 52 | defer d.Close() 53 | 54 | data := []byte(`{"id":"5","label":"contains","outV":"1", "inVs": ["2", "3"]}` + "\n") 55 | data = append(data, []byte(`{"id":"6","label":"contains","outV":"1", "inVs": [4]}`+"\n")...) 56 | 57 | require.NoError(t, d.Parse(bytes.NewReader(data))) 58 | 59 | require.Equal(t, []Id{2, 3, 4}, d.DocRanges[1]) 60 | } 61 | 62 | func TestParsingVeryLongLine(t *testing.T) { 63 | d, err := NewDocs() 64 | require.NoError(t, err) 65 | defer d.Close() 66 | 67 | line := []byte(`{"id": "` + strings.Repeat("a", 64*1024) + `"}`) 68 | 69 | require.NoError(t, d.Parse(bytes.NewReader(line))) 70 | } 71 | -------------------------------------------------------------------------------- /parser/lsif/parser_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "context" 7 | "encoding/json" 8 | "io" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestGenerate(t *testing.T) { 17 | t.Skip() 18 | filePath := "testdata/dump.lsif.zip" 19 | tmpDir := filePath + ".tmp" 20 | defer os.RemoveAll(tmpDir) 21 | 22 | createFiles(t, filePath, tmpDir) 23 | 24 | verifyCorrectnessOf(t, tmpDir, "lsif/main.go.json") 25 | verifyCorrectnessOf(t, tmpDir, "lsif/morestrings/reverse.go.json") 26 | } 27 | 28 | func verifyCorrectnessOf(t *testing.T, tmpDir, fileName string) { 29 | file, err := os.ReadFile(filepath.Join(tmpDir, fileName)) 30 | require.NoError(t, err) 31 | 32 | var buf bytes.Buffer 33 | require.NoError(t, json.Indent(&buf, file, "", " ")) 34 | 35 | expected, err := os.ReadFile(filepath.Join("testdata/expected/", fileName)) 36 | require.NoError(t, err) 37 | 38 | require.Equal(t, string(expected), buf.String()) 39 | } 40 | 41 | func createFiles(t *testing.T, filePath, tmpDir string) { 42 | t.Helper() 43 | file, err := os.Open(filePath) 44 | require.NoError(t, err) 45 | 46 | parser, err := NewParser(context.Background(), file) 47 | require.NoError(t, err) 48 | 49 | zipFileName := tmpDir + ".zip" 50 | w, err := os.Create(zipFileName) 51 | require.NoError(t, err) 52 | defer os.RemoveAll(zipFileName) 53 | 54 | _, err = io.Copy(w, parser) 55 | require.NoError(t, err) 56 | require.NoError(t, parser.Close()) 57 | 58 | extractZipFiles(t, tmpDir, zipFileName) 59 | } 60 | 61 | func extractZipFiles(t *testing.T, tmpDir, zipFileName string) { 62 | zipReader, err := zip.OpenReader(zipFileName) 63 | require.NoError(t, err) 64 | 65 | for _, file := range zipReader.Reader.File { 66 | zippedFile, err := file.Open() 67 | require.NoError(t, err) 68 | defer zippedFile.Close() 69 | 70 | fileDir, fileName := filepath.Split(file.Name) 71 | require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, fileDir), os.ModePerm)) 72 | 73 | outputFile, err := os.Create(filepath.Join(tmpDir, fileDir, fileName)) 74 | require.NoError(t, err) 75 | defer outputFile.Close() 76 | 77 | _, err = io.Copy(outputFile, zippedFile) 78 | require.NoError(t, err) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /parser/lsif/parser.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "archive/zip" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | ) 11 | 12 | var Lsif = "lsif" 13 | 14 | type Parser struct { 15 | Docs *Docs 16 | 17 | pr *io.PipeReader 18 | } 19 | 20 | func NewParserRaw(ctx context.Context, r io.Reader) (*Parser, error) { 21 | docs, err := NewDocs() 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | if err := docs.Parse(r); err != nil { 27 | return nil, err 28 | } 29 | 30 | pr, pw := io.Pipe() 31 | parser := &Parser{ 32 | Docs: docs, 33 | pr: pr, 34 | } 35 | 36 | err = pw.Close() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return parser, nil 42 | } 43 | 44 | func NewParser(ctx context.Context, r io.Reader) (*Parser, error) { 45 | docs, err := NewDocs() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | // ZIP files need to be seekable. Don't hold it all in RAM, use a tempfile 51 | tempFile, err := os.CreateTemp("", Lsif) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | size, err := io.Copy(tempFile, r) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | zr, err := zip.NewReader(tempFile, size) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | if len(zr.File) == 0 { 67 | return nil, errors.New("empty zip file") 68 | } 69 | 70 | file, err := zr.File[0].Open() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | defer file.Close() 76 | 77 | if err := docs.Parse(file); err != nil { 78 | return nil, err 79 | } 80 | 81 | pr, pw := io.Pipe() 82 | parser := &Parser{ 83 | Docs: docs, 84 | pr: pr, 85 | } 86 | 87 | go parser.transform(pw) 88 | 89 | return parser, nil 90 | } 91 | 92 | func (p *Parser) Read(b []byte) (int, error) { 93 | return p.pr.Read(b) 94 | } 95 | 96 | func (p *Parser) Close() error { 97 | p.pr.Close() 98 | 99 | return p.Docs.Close() 100 | } 101 | 102 | func (p *Parser) transform(pw *io.PipeWriter) { 103 | zw := zip.NewWriter(pw) 104 | 105 | if err := p.Docs.SerializeEntries(zw); err != nil { 106 | zw.Close() // Free underlying resources only 107 | pw.CloseWithError(fmt.Errorf("lsif parser: Docs.SerializeEntries: %v", err)) 108 | return 109 | } 110 | 111 | if err := zw.Close(); err != nil { 112 | pw.CloseWithError(fmt.Errorf("lsif parser: ZipWriter.Close: %v", err)) 113 | return 114 | } 115 | 116 | pw.Close() 117 | } 118 | -------------------------------------------------------------------------------- /matrix/api.go: -------------------------------------------------------------------------------- 1 | package matrix 2 | 3 | import ( 4 | "github.com/dominikbraun/graph" 5 | "github.com/williamfzc/srctx/graph/common" 6 | "gonum.org/v1/gonum/mat" 7 | ) 8 | 9 | const ( 10 | InvalidIndex = -1 11 | InvalidValue = -1.0 12 | ) 13 | 14 | /* 15 | Matrix 16 | 17 | Standard relationship representation layer based on matrix 18 | */ 19 | type Matrix struct { 20 | IndexMap map[string]int 21 | ReverseIndexMap map[int]string 22 | Data *mat.Dense 23 | } 24 | 25 | func (m *Matrix) Size() int { 26 | return len(m.IndexMap) 27 | } 28 | 29 | func (m *Matrix) Id(s string) int { 30 | if item, ok := m.IndexMap[s]; ok { 31 | return item 32 | } 33 | return InvalidIndex 34 | } 35 | 36 | func (m *Matrix) ById(id int) string { 37 | if item, ok := m.ReverseIndexMap[id]; ok { 38 | return item 39 | } 40 | return "" 41 | } 42 | 43 | func (m *Matrix) ForEach(s string, f func(i int, v float64)) { 44 | index := m.Id(s) 45 | if index == InvalidIndex { 46 | return 47 | } 48 | 49 | for i := 0; i < m.Size(); i++ { 50 | f(i, m.Data.At(i, index)) 51 | } 52 | } 53 | 54 | func CreateMatrixFromGraph[T string, U any](g graph.Graph[T, U]) (*Matrix, error) { 55 | adjacencyMap, err := g.AdjacencyMap() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | nodeCount := len(adjacencyMap) 61 | data := mat.NewDense(nodeCount, nodeCount, nil) 62 | 63 | indexMap := make(map[string]int) 64 | i := 0 65 | for node := range adjacencyMap { 66 | indexMap[string(node)] = i 67 | i++ 68 | } 69 | 70 | for source, edges := range adjacencyMap { 71 | sourceIndex := indexMap[string(source)] 72 | 73 | data.Set(sourceIndex, sourceIndex, float64(len(edges))) 74 | 75 | for target, edge := range edges { 76 | storage := edge.Properties.Data.(*common.EdgeStorage) 77 | targetIndex := indexMap[string(target)] 78 | 79 | currentValue := data.At(targetIndex, sourceIndex) 80 | data.Set(targetIndex, sourceIndex, currentValue+float64(len(storage.RefLines))) 81 | } 82 | } 83 | 84 | ret := &Matrix{ 85 | IndexMap: indexMap, 86 | ReverseIndexMap: reverseMap(indexMap), 87 | Data: data, 88 | } 89 | return ret, nil 90 | } 91 | 92 | func reverseMap(originalMap map[string]int) map[int]string { 93 | reversedMap := make(map[int]string) 94 | 95 | for key, value := range originalMap { 96 | reversedMap[value] = key 97 | } 98 | 99 | return reversedMap 100 | } 101 | -------------------------------------------------------------------------------- /object/source_context.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "github.com/alecthomas/chroma/v2" 5 | "github.com/dominikbraun/graph" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/williamfzc/srctx/parser/lsif" 8 | ) 9 | 10 | type ( 11 | FactKind = string 12 | RelKind = string 13 | ) 14 | 15 | const ( 16 | EdgeTypeName = "label" 17 | 18 | FactFile FactKind = "file" 19 | FactDef FactKind = "def" 20 | 21 | RelContains RelKind = "contains" 22 | RelReference RelKind = "reference" 23 | ) 24 | 25 | var ( 26 | EdgeAttrContains = graph.EdgeAttribute(EdgeTypeName, RelContains) 27 | EdgeAttrReference = graph.EdgeAttribute(EdgeTypeName, RelReference) 28 | ) 29 | 30 | type FactVertex struct { 31 | DocId int 32 | FileId int 33 | Kind FactKind 34 | Range *lsif.Range 35 | Extras interface{} 36 | } 37 | 38 | type FileExtras struct { 39 | Path string 40 | } 41 | 42 | type DefExtras struct { 43 | DefType string 44 | RawTokens []chroma.Token 45 | } 46 | 47 | func (v *FactVertex) Id() int { 48 | return v.DocId 49 | } 50 | 51 | func (v *FactVertex) LineNumber() int { 52 | return v.IndexLineNumber() + 1 53 | } 54 | 55 | func (v *FactVertex) IndexLineNumber() int { 56 | return int(v.Range.Line) 57 | } 58 | 59 | func (v *FactVertex) ToRelVertex() *RelVertex { 60 | return &RelVertex{ 61 | DocId: v.DocId, 62 | FileId: v.FileId, 63 | Kind: v.Kind, 64 | Range: v.Range, 65 | } 66 | } 67 | 68 | type RelVertex struct { 69 | DocId int 70 | FileId int 71 | Kind FactKind 72 | Range *lsif.Range 73 | } 74 | 75 | func (v *RelVertex) Id() int { 76 | return v.DocId 77 | } 78 | 79 | func (v *RelVertex) LineNumber() int { 80 | rangeObj := v.Range 81 | if rangeObj == nil { 82 | log.Warnf("range is nil: %v", v) 83 | return -1 84 | } 85 | return int(rangeObj.Line + 1) 86 | } 87 | 88 | func (v *RelVertex) CharNumber() int { 89 | return int(v.Range.Character + 1) 90 | } 91 | 92 | type SourceContext struct { 93 | FileMapping map[string]int 94 | FactGraph graph.Graph[int, *FactVertex] 95 | RelGraph graph.Graph[int, *RelVertex] 96 | 97 | // caches 98 | FactAdjMap map[int]map[int]graph.Edge[int] 99 | RelAdjMap map[int]map[int]graph.Edge[int] 100 | } 101 | 102 | func NewSourceContext() SourceContext { 103 | factGraph := graph.New((*FactVertex).Id, graph.Directed()) 104 | relGraph := graph.New((*RelVertex).Id, graph.Directed()) 105 | 106 | return SourceContext{ 107 | FileMapping: make(map[string]int), 108 | FactGraph: factGraph, 109 | RelGraph: relGraph, 110 | FactAdjMap: nil, 111 | RelAdjMap: nil, 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /scripts/quickstart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # Set version number and base url 6 | version="0.11.0" 7 | base_url="https://github.com/williamfzc/srctx/releases/download" 8 | 9 | echo "Starting SRCTX..." 10 | 11 | # Determine the filename of the srctx executable based on the OS type and architecture 12 | if [[ "$OSTYPE" == "darwin"* ]]; then 13 | # Mac OS 14 | if [[ "$(uname -m)" == "arm64" ]]; then 15 | # M1 Mac 16 | filename="srctx-darwin-arm64" 17 | else 18 | # Intel Mac 19 | filename="srctx-darwin-amd64" 20 | fi 21 | elif [[ "$OSTYPE" == "linux-gnu"* ]]; then 22 | # Linux 23 | if [[ "$(uname -m)" == "aarch64" ]]; then 24 | # ARM 64 25 | filename="srctx-linux-arm64" 26 | else 27 | # x86_64 28 | filename="srctx-linux-amd64" 29 | fi 30 | elif [[ "$OSTYPE" == "msys" ]]; then 31 | # Windows 32 | filename="srctx-windows-amd64.exe" 33 | else 34 | echo "Unsupported OS type: $OSTYPE" 35 | exit 1 36 | fi 37 | 38 | # Check if the srctx executable already exists 39 | if [[ ! -f "srctx_bin" ]]; then 40 | echo "Downloading srctx executable..." 41 | # Download the srctx executable 42 | wget "${base_url}/v${version}/${filename}" -O srctx_bin 43 | chmod +x srctx_bin 44 | fi 45 | 46 | # Run srctx on the specified file or directory, with language parameter 47 | if [[ "$SRCTX_LANG" == "GOLANG" ]]; then 48 | echo "Running srctx for Golang..." 49 | ./srctx_bin diff --lang GOLANG --withIndex --src "$SRCTX_SRC" --outputHtml ./output.html 50 | 51 | elif [[ "$SRCTX_LANG" == "JAVA" ]]; then 52 | echo "Running srctx for Java..." 53 | # Download and unpack scip-java.zip 54 | if [[ ! -f "scip-java.zip" ]]; then 55 | echo "Downloading scip-java.zip..." 56 | # we do not always ship this zip 57 | wget "${base_url}/v0.8.0/scip-java.zip" 58 | fi 59 | echo "Extracting scip-java.zip..." 60 | unzip -o scip-java.zip 61 | 62 | ./scip-java index "$SRCTX_BUILD_CMD" 63 | ./srctx_bin diff --lang JAVA --src "$SRCTX_SRC" --scip ./index.scip --outputHtml ./output.html 64 | 65 | elif [[ "$SRCTX_LANG" == "KOTLIN" ]]; then 66 | echo "Running srctx for Kotlin..." 67 | # Download and unpack scip-java.zip 68 | if [[ ! -f "scip-java.zip" ]]; then 69 | echo "Downloading scip-java.zip..." 70 | # we do not always ship this zip 71 | wget "${base_url}/v0.8.0/scip-java.zip" 72 | fi 73 | echo "Extracting scip-java.zip..." 74 | unzip -o scip-java.zip 75 | 76 | ./scip-java index "$SRCTX_BUILD_CMD" 77 | ./srctx_bin diff --lang KOTLIN --src "$SRCTX_SRC" --scip ./index.scip --outputHtml ./output.html 78 | 79 | else 80 | echo "Unsupported language: $SRCTX_LANG" 81 | exit 1 82 | fi 83 | 84 | echo "SRCTX finished successfully." 85 | -------------------------------------------------------------------------------- /graph/function/api_transform.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/dominikbraun/graph" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/williamfzc/srctx/graph/file" 7 | ) 8 | 9 | func (fg *Graph) ToFileGraph() (*file.Graph, error) { 10 | // create graph 11 | fileGraph := &file.Graph{ 12 | G: graph.New((*file.Vertex).Id, graph.Directed()), 13 | Rg: graph.New((*file.Vertex).Id, graph.Directed()), 14 | } 15 | // building edges 16 | err := FuncGraph2FileGraph(fg.g, fileGraph.G) 17 | if err != nil { 18 | return nil, err 19 | } 20 | err = FuncGraph2FileGraph(fg.rg, fileGraph.Rg) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | nodeCount, err := fileGraph.G.Order() 26 | if err != nil { 27 | return nil, err 28 | } 29 | edgeCount, err := fileGraph.G.Size() 30 | if err != nil { 31 | return nil, err 32 | } 33 | log.Infof("file graph ready. nodes: %d, edges: %d", nodeCount, edgeCount) 34 | 35 | return fileGraph, nil 36 | } 37 | 38 | func (fg *Graph) ToDirGraph() (*file.Graph, error) { 39 | fileGraph, err := fg.ToFileGraph() 40 | if err != nil { 41 | return nil, err 42 | } 43 | return fileGraph.ToDirGraph() 44 | } 45 | 46 | func FuncGraph2FileGraph(f graph.Graph[string, *Vertex], g graph.Graph[string, *file.Vertex]) error { 47 | m, err := f.AdjacencyMap() 48 | if err != nil { 49 | return err 50 | } 51 | // add all the vertices 52 | for k := range m { 53 | v, err := f.Vertex(k) 54 | if err != nil { 55 | return err 56 | } 57 | _ = g.AddVertex(file.Path2vertex(v.Path)) 58 | } 59 | 60 | edges, err := f.Edges() 61 | if err != nil { 62 | return err 63 | } 64 | for _, eachEdge := range edges { 65 | source, err := f.Vertex(eachEdge.Source) 66 | if err != nil { 67 | log.Warnf("vertex not found: %v", eachEdge.Source) 68 | continue 69 | } 70 | target, err := f.Vertex(eachEdge.Target) 71 | if err != nil { 72 | log.Warnf("vertex not found: %v", eachEdge.Target) 73 | continue 74 | } 75 | 76 | // ignore self ptr 77 | if source.Path == target.Path { 78 | continue 79 | } 80 | 81 | sourceFile, err := g.Vertex(source.Path) 82 | if err != nil { 83 | return err 84 | } 85 | targetFile, err := g.Vertex(target.Path) 86 | if err != nil { 87 | return err 88 | } 89 | if sv, err := g.Vertex(sourceFile.Id()); err == nil { 90 | sv.Referenced++ 91 | } else { 92 | _ = g.AddVertex(sourceFile) 93 | } 94 | if tv, err := g.Vertex(targetFile.Id()); err == nil { 95 | tv.Referenced++ 96 | } else { 97 | _ = g.AddVertex(targetFile) 98 | } 99 | _ = g.AddEdge(sourceFile.Id(), targetFile.Id()) 100 | } 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /graph/visual/g6/object.go: -------------------------------------------------------------------------------- 1 | package g6 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "os" 7 | "text/template" 8 | 9 | "github.com/williamfzc/srctx" 10 | 11 | "github.com/goccy/go-json" 12 | ) 13 | 14 | //go:embed template/report.html 15 | var g6ReportTemplate string 16 | 17 | type Node struct { 18 | Id string `json:"id"` 19 | Label string `json:"label,omitempty"` 20 | ComboId string `json:"comboId,omitempty"` 21 | Style *NodeStyle `json:"style"` 22 | } 23 | 24 | // NodeStyle https://g6.antv.antgroup.com/api/shape-properties 25 | type NodeStyle struct { 26 | Fill string `json:"fill,omitempty"` 27 | } 28 | 29 | type Edge struct { 30 | Source string `json:"source"` 31 | Target string `json:"target"` 32 | } 33 | 34 | type Combo struct { 35 | Id string `json:"id"` 36 | Label string `json:"label"` 37 | Collapsed bool `json:"collapsed,omitempty"` 38 | ParentId string `json:"parentId,omitempty"` 39 | } 40 | 41 | // Data https://g6.antv.antgroup.com/api/graph-func/data 42 | type Data struct { 43 | Nodes []*Node `json:"nodes"` 44 | Edges []*Edge `json:"edges"` 45 | Combos []*Combo `json:"combos"` 46 | } 47 | 48 | func EmptyG6Data() *Data { 49 | return &Data{ 50 | Nodes: make([]*Node, 0), 51 | Edges: make([]*Edge, 0), 52 | Combos: make([]*Combo, 0), 53 | } 54 | } 55 | 56 | type renderData struct { 57 | Version string 58 | Url string 59 | Data string 60 | } 61 | 62 | func (g *Data) RenderHtml(filename string) error { 63 | // render 64 | dataRaw, err := json.Marshal(g) 65 | if err != nil { 66 | return nil 67 | } 68 | 69 | parsed, err := template.New("").Parse(g6ReportTemplate) 70 | if err != nil { 71 | return err 72 | } 73 | var buf bytes.Buffer 74 | err = parsed.Execute(&buf, &renderData{ 75 | Version: srctx.Version, 76 | Url: srctx.RepoUrl, 77 | Data: string(dataRaw), 78 | }) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | err = os.WriteFile(filename, buf.Bytes(), 0o666) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (g *Data) FillWithOrange(label string) { 92 | for _, each := range g.Nodes { 93 | if each.Label == label { 94 | each.Style.Fill = "orange" 95 | break 96 | } 97 | } 98 | } 99 | 100 | func (g *Data) FillWithYellow(label string) { 101 | for _, each := range g.Nodes { 102 | if each.Label == label { 103 | each.Style.Fill = "yellow" 104 | break 105 | } 106 | } 107 | } 108 | 109 | func (g *Data) FillWithRed(label string) { 110 | for _, each := range g.Nodes { 111 | if each.Label == label { 112 | each.Style.Fill = "red" 113 | break 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /graph/function/api_query_ctx.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "github.com/dominikbraun/graph" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // DirectReferencedCount 9 | // This function returns the number of direct references to a given function vertex in the function graph. 10 | // It does so by counting the length of the slice of IDs of the function vertices that directly reference the given function vertex. 11 | func (fg *Graph) DirectReferencedCount(f *Vertex) int { 12 | return len(fg.DirectReferencedIds(f)) 13 | } 14 | 15 | func (fg *Graph) DirectReferencedIds(f *Vertex) []string { 16 | adjacencyMap, err := fg.g.AdjacencyMap() 17 | if err != nil { 18 | log.Warnf("failed to get adjacency map: %v", f) 19 | return nil 20 | } 21 | m := adjacencyMap[f.Id()] 22 | ret := make([]string, 0, len(m)) 23 | for k := range m { 24 | ret = append(ret, k) 25 | } 26 | return ret 27 | } 28 | 29 | func (fg *Graph) DirectReferenceIds(f *Vertex) []string { 30 | adjacencyMap, err := fg.rg.AdjacencyMap() 31 | if err != nil { 32 | log.Warnf("failed to get adjacency map: %v", f) 33 | return nil 34 | } 35 | m := adjacencyMap[f.Id()] 36 | ret := make([]string, 0, len(m)) 37 | for k := range m { 38 | ret = append(ret, k) 39 | } 40 | return ret 41 | } 42 | 43 | // TransitiveReferencedIds 44 | // This function takes a Graph and a Vertex as input and returns a slice of strings containing all the transitive referenced ids. 45 | // It uses a map to store the referenced ids and a BFS algorithm to traverse the graph and add the referenced ids to the map. 46 | // Finally, it returns the keys of the map as a slice of strings. 47 | func (fg *Graph) TransitiveReferencedIds(f *Vertex) []string { 48 | m := make(map[string]struct{}, 0) 49 | start := f.Id() 50 | graph.BFS(fg.g, start, func(cur string) bool { 51 | if cur == start { 52 | return false 53 | } 54 | m[cur] = struct{}{} 55 | return false 56 | }) 57 | ret := make([]string, 0, len(m)) 58 | for k := range m { 59 | ret = append(ret, k) 60 | } 61 | return ret 62 | } 63 | 64 | func (fg *Graph) TransitiveReferenceIds(f *Vertex) []string { 65 | m := make(map[string]struct{}, 0) 66 | start := f.Id() 67 | graph.BFS(fg.rg, start, func(cur string) bool { 68 | if cur == start { 69 | return false 70 | } 71 | m[cur] = struct{}{} 72 | return false 73 | }) 74 | ret := make([]string, 0, len(m)) 75 | for k := range m { 76 | ret = append(ret, k) 77 | } 78 | return ret 79 | } 80 | 81 | func (fg *Graph) EntryIds(f *Vertex) []string { 82 | ret := make([]string, 0) 83 | // and also itself 84 | all := append(fg.TransitiveReferencedIds(f), f.Id()) 85 | for _, eachId := range all { 86 | item := fg.IdCache[eachId] 87 | if item.ContainTag(TagEntry) { 88 | ret = append(ret, eachId) 89 | } 90 | } 91 | return ret 92 | } 93 | -------------------------------------------------------------------------------- /parser/lsif/references.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | type ReferencesOffset struct { 8 | Id Id 9 | Len int32 10 | } 11 | 12 | type References struct { 13 | Items Cache 14 | Offsets Cache 15 | CurrentOffsetId Id 16 | } 17 | 18 | type SerializedReference struct { 19 | Path string `json:"path"` 20 | } 21 | 22 | func NewReferences() (*References, error) { 23 | items, err := newCache("references", Item{}) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | offsets, err := newCache("references-offsets", ReferencesOffset{}) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return &References{ 34 | Items: items, 35 | Offsets: offsets, 36 | CurrentOffsetId: 0, 37 | }, nil 38 | } 39 | 40 | // Store is responsible for keeping track of references that will be used when 41 | // serializing in `For`. 42 | // 43 | // The references are stored in a file to cacheMem them. It is like 44 | // `map[Id][]Item` (where `Id` is `refId`) but relies on caching the array and 45 | // its offset in files for storage to reduce RAM usage. The items can be 46 | // fetched by calling `GetItems`. 47 | func (r *References) Store(refId Id, references []Item) error { 48 | size := len(references) 49 | 50 | if size == 0 { 51 | return nil 52 | } 53 | 54 | items := append(r.GetItems(refId), references...) 55 | err := r.Items.SetEntry(r.CurrentOffsetId, items) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | size = len(items) 61 | r.Offsets.SetEntry(refId, ReferencesOffset{Id: r.CurrentOffsetId, Len: int32(size)}) 62 | r.CurrentOffsetId += Id(size) 63 | 64 | return nil 65 | } 66 | 67 | func (r *References) For(docs map[Id]string, refId Id) []SerializedReference { 68 | references := r.GetItems(refId) 69 | if references == nil { 70 | return nil 71 | } 72 | 73 | var serializedReferences []SerializedReference 74 | 75 | for _, reference := range references { 76 | serializedReference := SerializedReference{ 77 | Path: docs[reference.DocId] + "#L" + strconv.Itoa(int(reference.Line)), 78 | } 79 | 80 | serializedReferences = append(serializedReferences, serializedReference) 81 | } 82 | 83 | return serializedReferences 84 | } 85 | 86 | func (r *References) Close() error { 87 | for _, err := range []error{ 88 | r.Items.Close(), 89 | r.Offsets.Close(), 90 | } { 91 | if err != nil { 92 | return err 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func (r *References) GetItems(refId Id) []Item { 99 | var offset ReferencesOffset 100 | if err := r.Offsets.Entry(refId, &offset); err != nil || offset.Len == 0 { 101 | return nil 102 | } 103 | 104 | items := make([]Item, offset.Len) 105 | if err := r.Items.Entry(offset.Id, &items); err != nil { 106 | return nil 107 | } 108 | 109 | return items 110 | } 111 | -------------------------------------------------------------------------------- /object/ctx_rel.go: -------------------------------------------------------------------------------- 1 | package object 2 | 3 | import ( 4 | "fmt" 5 | 6 | log "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func (sc *SourceContext) RefsByFileName(fileName string) ([]*RelVertex, error) { 10 | // get all the reference points in this file 11 | fileId := sc.FileId(fileName) 12 | if fileId == 0 { 13 | return nil, fmt.Errorf("no file named: %s", fileName) 14 | } 15 | 16 | // collect all the nodes starting from this file 17 | startPoints := make([]*RelVertex, 0) 18 | for each := range sc.FactAdjMap[fileId] { 19 | factVertex, err := sc.FactGraph.Vertex(each) 20 | if err != nil { 21 | return nil, err 22 | } 23 | startPoints = append(startPoints, factVertex.ToRelVertex()) 24 | } 25 | 26 | return startPoints, nil 27 | } 28 | 29 | func (sc *SourceContext) RefsByLine(fileName string, lineNum int) ([]*RelVertex, error) { 30 | allVertexes, err := sc.RefsByFileName(fileName) 31 | if err != nil { 32 | return nil, err 33 | } 34 | log.Debugf("file %s refs: %d", fileName, len(allVertexes)) 35 | ret := make([]*RelVertex, 0) 36 | for _, each := range allVertexes { 37 | if each.LineNumber() == lineNum { 38 | ret = append(ret, each) 39 | } 40 | } 41 | if len(ret) == 0 { 42 | return nil, fmt.Errorf("no ref found in %s %d", fileName, lineNum) 43 | } 44 | return ret, nil 45 | } 46 | 47 | func (sc *SourceContext) RefsByLineAndChar(fileName string, lineNum int, charNum int) ([]*RelVertex, error) { 48 | allVertexes, err := sc.RefsByLine(fileName, lineNum) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | ret := make([]*RelVertex, 0) 54 | for _, each := range allVertexes { 55 | if int(each.Range.Character) == charNum { 56 | ret = append(ret, each) 57 | } 58 | } 59 | if len(ret) == 0 { 60 | return nil, fmt.Errorf("no ref found in %s %d", fileName, lineNum) 61 | } 62 | return ret, nil 63 | } 64 | 65 | func (sc *SourceContext) RefsFromDefId(defId int) ([]*FactVertex, error) { 66 | // check 67 | ret := make([]*FactVertex, 0) 68 | _, err := sc.RelGraph.Vertex(defId) 69 | if err != nil { 70 | // no ref info, it's ok 71 | return ret, nil 72 | } 73 | 74 | for each := range sc.RelAdjMap[defId] { 75 | vertex, err := sc.FactGraph.Vertex(each) 76 | if err != nil { 77 | return nil, err 78 | } 79 | ret = append(ret, vertex) 80 | } 81 | 82 | return ret, nil 83 | } 84 | 85 | func (sc *SourceContext) RefsFromLineWithLimit(fileName string, lineNum int, charLength int) ([]*FactVertex, error) { 86 | startPoints, err := sc.RefsByLine(fileName, lineNum) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | // search all the related points 92 | ret := make(map[int]*FactVertex, 0) 93 | for _, each := range startPoints { 94 | // optimize 95 | if int(each.Range.Length) != charLength { 96 | continue 97 | } 98 | 99 | curRet, err := sc.RefsFromDefId(each.Id()) 100 | if err != nil { 101 | return nil, err 102 | } 103 | for _, eachRef := range curRet { 104 | ret[eachRef.Id()] = eachRef 105 | } 106 | } 107 | 108 | final := make([]*FactVertex, 0, len(ret)) 109 | for _, v := range ret { 110 | final = append(final, v) 111 | } 112 | return final, nil 113 | } 114 | -------------------------------------------------------------------------------- /cmd/srctx/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/williamfzc/srctx/cmd/srctx/diff" 9 | ) 10 | 11 | func TestDiff(t *testing.T) { 12 | t.Run("default diff", func(t *testing.T) { 13 | mainFunc([]string{ 14 | "srctx", "diff", 15 | "--src", "../..", 16 | "--repoRoot", "../..", 17 | "--before", "HEAD~1", 18 | "--outputDot", "output.dot", 19 | "--outputCsv", "output.csv", 20 | "--outputJson", "output.json", 21 | "--cacheType", "mem", 22 | "--lsif", "../../dump.lsif", 23 | }) 24 | }) 25 | 26 | t.Run("raw diff", func(t *testing.T) { 27 | t.Skip("this case did not work in github action") 28 | mainFunc([]string{ 29 | "srctx", "diff", 30 | "--src", "../..", 31 | "--before", "HEAD~1", 32 | "--outputDot", "output.dot", 33 | "--outputCsv", "output.csv", 34 | "--outputJson", "output.json", 35 | "--withIndex", 36 | }) 37 | }) 38 | 39 | t.Run("file level diff", func(t *testing.T) { 40 | mainFunc([]string{ 41 | "srctx", "diff", 42 | "--src", "../..", 43 | "--before", "HEAD~1", 44 | "--outputDot", "output.dot", 45 | "--outputCsv", "output.csv", 46 | "--outputJson", "output.json", 47 | "--lsif", "../../dump.lsif", 48 | "--nodeLevel", "file", 49 | }) 50 | }) 51 | 52 | t.Run("specific language diff", func(t *testing.T) { 53 | mainFunc([]string{ 54 | "srctx", "diff", 55 | "--src", "../..", 56 | "--before", "HEAD~1", 57 | "--outputDot", "output.dot", 58 | "--outputCsv", "output.csv", 59 | "--outputJson", "output.json", 60 | "--lsif", "../../dump.lsif", 61 | "--lang", "GOLANG", 62 | }) 63 | }) 64 | 65 | t.Run("no diff", func(t *testing.T) { 66 | mainFunc([]string{ 67 | "srctx", "diff", 68 | "--src", "../..", 69 | "--outputDot", "output.dot", 70 | "--outputCsv", "output.csv", 71 | "--outputJson", "output.json", 72 | "--lsif", "../../dump.lsif", 73 | "--noDiff", 74 | }) 75 | }) 76 | 77 | t.Run("dump with existed file", func(t *testing.T) { 78 | mainFunc([]string{ 79 | "srctx", "dump", 80 | "--src", "../..", 81 | "--lsif", "../../dump.lsif", 82 | }) 83 | }) 84 | } 85 | 86 | func TestRenderHtml(t *testing.T) { 87 | t.Run("render func html", func(t *testing.T) { 88 | mainFunc([]string{ 89 | "srctx", "diff", 90 | "--src", "../..", 91 | "--before", "HEAD~1", 92 | "--outputHtml", "output.html", 93 | "--lsif", "../../dump.lsif", 94 | "--nodeLevel", "func", 95 | }) 96 | }) 97 | 98 | t.Run("render file html", func(t *testing.T) { 99 | mainFunc([]string{ 100 | "srctx", "diff", 101 | "--src", "../..", 102 | "--before", "HEAD~1", 103 | "--outputHtml", "output.html", 104 | "--lsif", "../../dump.lsif", 105 | "--nodeLevel", "file", 106 | }) 107 | }) 108 | } 109 | 110 | func TestDiffCfg(t *testing.T) { 111 | t.Run("generate default config file", func(t *testing.T) { 112 | mainFunc([]string{ 113 | "srctx", "diffcfg", 114 | }) 115 | defer os.Remove(diff.DefaultConfigFile) 116 | assert.FileExists(t, diff.DefaultConfigFile) 117 | }) 118 | } 119 | 120 | func TestDump(t *testing.T) { 121 | t.Run("dump", func(t *testing.T) { 122 | mainFunc([]string{"srctx", "dump", "--src", ".."}) 123 | }) 124 | } 125 | -------------------------------------------------------------------------------- /parser/lsif/cache.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "io" 7 | "os" 8 | ) 9 | 10 | // This cache implementation is using a temp file to provide key-value data storage 11 | // It allows to avoid storing intermediate calculations in RAM 12 | // The stored data must be a fixed-size value or a slice of fixed-size values, or a pointer to such data 13 | 14 | // 2023/05/21: 15 | // currently we still move it back to RAM 16 | // avoid too much IO harming my disk in development ... 17 | 18 | type Cache interface { 19 | SetEntry(id Id, data interface{}) error 20 | Entry(id Id, data interface{}) error 21 | Close() error 22 | setOffset(id Id) error 23 | GetReader() io.Reader 24 | } 25 | 26 | const ( 27 | CacheTypeFile = "file" 28 | CacheTypeMem = "mem" 29 | ) 30 | 31 | var CacheType = CacheTypeFile 32 | 33 | type cacheMem struct { 34 | file *File 35 | chunkSize int64 36 | } 37 | 38 | func newCache(filename string, data interface{}) (Cache, error) { 39 | switch CacheType { 40 | case CacheTypeMem: 41 | return newCacheMem(filename, data) 42 | case CacheTypeFile: 43 | return newCacheFile(filename, data) 44 | default: 45 | return nil, fmt.Errorf("invalid cache type: %v", CacheType) 46 | } 47 | } 48 | 49 | func newCacheMem(filename string, data interface{}) (Cache, error) { 50 | f := New([]byte{}) 51 | return &cacheMem{file: f, chunkSize: int64(binary.Size(data))}, nil 52 | } 53 | 54 | func (c *cacheMem) GetReader() io.Reader { 55 | return c.file 56 | } 57 | 58 | func (c *cacheMem) SetEntry(id Id, data interface{}) error { 59 | if err := c.setOffset(id); err != nil { 60 | return err 61 | } 62 | 63 | return binary.Write(c.file, binary.LittleEndian, data) 64 | } 65 | 66 | func (c *cacheMem) Entry(id Id, data interface{}) error { 67 | if err := c.setOffset(id); err != nil { 68 | return err 69 | } 70 | 71 | return binary.Read(c.file, binary.LittleEndian, data) 72 | } 73 | 74 | func (c *cacheMem) Close() error { 75 | // virtual file needs no `close` 76 | return nil 77 | } 78 | 79 | func (c *cacheMem) setOffset(id Id) error { 80 | offset := int64(id) * c.chunkSize 81 | _, err := c.file.Seek(offset, io.SeekStart) 82 | 83 | return err 84 | } 85 | 86 | type cacheFile struct { 87 | file *os.File 88 | chunkSize int64 89 | } 90 | 91 | func (c *cacheFile) GetReader() io.Reader { 92 | return c.file 93 | } 94 | 95 | func newCacheFile(filename string, data interface{}) (Cache, error) { 96 | f, err := os.CreateTemp("", filename) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | return &cacheFile{file: f, chunkSize: int64(binary.Size(data))}, nil 102 | } 103 | 104 | func (c *cacheFile) SetEntry(id Id, data interface{}) error { 105 | if err := c.setOffset(id); err != nil { 106 | return err 107 | } 108 | 109 | return binary.Write(c.file, binary.LittleEndian, data) 110 | } 111 | 112 | func (c *cacheFile) Entry(id Id, data interface{}) error { 113 | if err := c.setOffset(id); err != nil { 114 | return err 115 | } 116 | 117 | return binary.Read(c.file, binary.LittleEndian, data) 118 | } 119 | 120 | func (c *cacheFile) Close() error { 121 | return c.file.Close() 122 | } 123 | 124 | func (c *cacheFile) setOffset(id Id) error { 125 | offset := int64(id) * c.chunkSize 126 | _, err := c.file.Seek(offset, io.SeekStart) 127 | 128 | return err 129 | } 130 | -------------------------------------------------------------------------------- /cmd/srctx/diff/core.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/williamfzc/srctx/diff" 12 | "github.com/williamfzc/srctx/object" 13 | "github.com/williamfzc/srctx/parser" 14 | "github.com/williamfzc/srctx/parser/lsif" 15 | ) 16 | 17 | // MainDiff allow accessing as a lib 18 | func MainDiff(opts *Options) error { 19 | log.Infof("start diffing: %v", opts.Src) 20 | 21 | if opts.CacheType != lsif.CacheTypeFile { 22 | parser.UseMemCache() 23 | } 24 | 25 | // collect diff info 26 | lineMap, err := collectLineMap(opts) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // collect info from file (line number/size ...) 32 | totalLineCountMap, err := collectTotalLineCountMap(opts, opts.Src, lineMap) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | err = createIndexFile(opts) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | switch opts.NodeLevel { 43 | case object.NodeLevelFunc: 44 | err = funcLevelMain(opts, lineMap, totalLineCountMap) 45 | if err != nil { 46 | return err 47 | } 48 | case object.NodeLevelFile: 49 | err = fileLevelMain(opts, lineMap, totalLineCountMap) 50 | if err != nil { 51 | return err 52 | } 53 | } 54 | 55 | log.Infof("everything done.") 56 | return nil 57 | } 58 | 59 | func createIndexFile(opts *Options) error { 60 | if opts.IndexCmd == "" { 61 | return nil 62 | } 63 | 64 | log.Infof("create index file with cmd: %v", opts.IndexCmd) 65 | 66 | parts := strings.Split(opts.IndexCmd, " ") 67 | cmd := exec.Command(parts[0], parts[1:]...) 68 | cmd.Stdout = os.Stdout 69 | cmd.Stderr = os.Stderr 70 | err := cmd.Run() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func collectLineMap(opts *Options) (diff.ImpactLineMap, error) { 79 | if !opts.NoDiff { 80 | lineMap, err := diff.GitDiff(opts.Src, opts.Before, opts.After) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return lineMap, nil 85 | } 86 | log.Infof("noDiff enabled") 87 | return make(diff.ImpactLineMap), nil 88 | } 89 | 90 | func collectTotalLineCountMap(opts *Options, src string, lineMap diff.ImpactLineMap) (map[string]int, error) { 91 | totalLineCountMap := make(map[string]int) 92 | var err error 93 | 94 | if opts.RepoRoot != "" { 95 | repoRoot, err := filepath.Abs(opts.RepoRoot) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | log.Infof("path sync from %s to %s", repoRoot, src) 101 | lineMap, err = diff.PathOffset(repoRoot, src, lineMap) 102 | if err != nil { 103 | return nil, err 104 | } 105 | } 106 | 107 | for eachPath := range lineMap { 108 | totalLineCountMap[eachPath], err = lineCounter(filepath.Join(src, eachPath)) 109 | if err != nil { 110 | // ignore this err, files can be removed/moved 111 | log.Infof("file has been removed: %s, set line counter to 0", eachPath) 112 | } 113 | } 114 | 115 | return totalLineCountMap, nil 116 | } 117 | 118 | // https://stackoverflow.com/a/24563853 119 | func lineCounter(fileName string) (int, error) { 120 | file, err := os.Open(fileName) 121 | if err != nil { 122 | return 0, err 123 | } 124 | fileScanner := bufio.NewScanner(file) 125 | lineCount := 0 126 | for fileScanner.Scan() { 127 | lineCount++ 128 | } 129 | return lineCount, nil 130 | } 131 | -------------------------------------------------------------------------------- /graph/function/api_stat.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import "github.com/williamfzc/srctx/object" 4 | 5 | func (fg *Graph) Stat(f *Vertex) *object.ImpactUnit { 6 | referenceIds := fg.DirectReferenceIds(f) 7 | referencedIds := fg.DirectReferencedIds(f) 8 | 9 | transitiveReferencedIds := fg.TransitiveReferencedIds(f) 10 | transitiveReferenceIds := fg.TransitiveReferenceIds(f) 11 | entries := fg.EntryIds(f) 12 | 13 | impactUnit := object.NewImpactUnit() 14 | 15 | impactUnit.FileName = f.Path 16 | impactUnit.UnitName = f.Id() 17 | 18 | impactUnit.ImpactCount = len(referencedIds) 19 | impactUnit.TransImpactCount = len(transitiveReferencedIds) 20 | impactUnit.ImpactEntries = len(entries) 21 | 22 | // details 23 | impactUnit.Self = f 24 | impactUnit.ReferenceIds = referenceIds 25 | impactUnit.ReferencedIds = referencedIds 26 | impactUnit.TransitiveReferenceIds = transitiveReferenceIds 27 | impactUnit.TransitiveReferencedIds = transitiveReferencedIds 28 | impactUnit.Entries = entries 29 | 30 | return impactUnit 31 | } 32 | 33 | func (fg *Graph) GlobalStat(points []*Vertex) *object.StatGlobal { 34 | sg := &object.StatGlobal{ 35 | UnitLevel: object.NodeLevelFile, 36 | UnitMapping: make(map[string]int), 37 | } 38 | 39 | // creating mapping 40 | curId := 0 41 | for each := range fg.IdCache { 42 | sg.UnitMapping[each] = curId 43 | curId++ 44 | } 45 | 46 | entries := fg.ListEntries() 47 | totalEntries := make([]int, 0, len(entries)) 48 | for _, each := range entries { 49 | eachId := sg.UnitMapping[each.Id()] 50 | totalEntries = append(totalEntries, eachId) 51 | } 52 | sg.TotalEntries = totalEntries 53 | 54 | stats := make(map[int]*object.ImpactUnit, 0) 55 | for _, each := range points { 56 | eachStat := fg.Stat(each) 57 | eachId := sg.UnitMapping[each.Id()] 58 | stats[eachId] = eachStat 59 | } 60 | sg.ImpactUnitsMap = stats 61 | 62 | // direct impact 63 | directImpactMap := make(map[int]struct{}) 64 | for _, each := range stats { 65 | for _, eachReferenced := range each.ReferencedIds { 66 | eachId := sg.UnitMapping[eachReferenced] 67 | directImpactMap[eachId] = struct{}{} 68 | } 69 | // and itself 70 | itselfId := sg.UnitMapping[each.Self.Id()] 71 | directImpactMap[itselfId] = struct{}{} 72 | } 73 | directImpactList := make([]int, 0, len(directImpactMap)) 74 | for each := range directImpactMap { 75 | directImpactList = append(directImpactList, each) 76 | } 77 | sg.ImpactUnits = directImpactList 78 | 79 | // in-direct impact 80 | indirectImpactMap := make(map[int]struct{}) 81 | for _, each := range stats { 82 | for _, eachReferenced := range each.TransitiveReferencedIds { 83 | eachId := sg.UnitMapping[eachReferenced] 84 | indirectImpactMap[eachId] = struct{}{} 85 | } 86 | // and itself 87 | itselfId := sg.UnitMapping[each.Self.Id()] 88 | indirectImpactMap[itselfId] = struct{}{} 89 | } 90 | indirectImpactList := make([]int, 0, len(indirectImpactMap)) 91 | for each := range indirectImpactMap { 92 | indirectImpactList = append(indirectImpactList, each) 93 | } 94 | sg.TransImpactUnits = indirectImpactList 95 | 96 | // entries 97 | entriesMap := make(map[int]struct{}) 98 | for _, each := range stats { 99 | for _, eachEntry := range each.Entries { 100 | eachId := sg.UnitMapping[eachEntry] 101 | entriesMap[eachId] = struct{}{} 102 | } 103 | } 104 | entriesList := make([]int, 0, len(entriesMap)) 105 | for each := range entriesMap { 106 | entriesList = append(entriesList, each) 107 | } 108 | sg.ImpactEntries = entriesList 109 | 110 | return sg 111 | } 112 | -------------------------------------------------------------------------------- /parser/lsif/hovers.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | ) 7 | 8 | type Offset struct { 9 | At int32 10 | Len int32 11 | } 12 | 13 | type Hovers struct { 14 | File *os.File 15 | Offsets Cache 16 | CurrentOffset int 17 | } 18 | 19 | type RawResult struct { 20 | Contents json.RawMessage `json:"contents"` 21 | } 22 | 23 | type RawData struct { 24 | Id Id `json:"id"` 25 | Result RawResult `json:"result"` 26 | } 27 | 28 | type HoverRef struct { 29 | ResultSetId Id `json:"outV"` 30 | HoverId Id `json:"inV"` 31 | } 32 | 33 | type ResultSetRef struct { 34 | ResultSetId Id `json:"outV"` 35 | RefId Id `json:"inV"` 36 | } 37 | 38 | func NewHovers() (*Hovers, error) { 39 | file, err := os.CreateTemp("", "hovers") 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | offsets, err := newCache("hovers-indexes", Offset{}) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &Hovers{ 50 | File: file, 51 | Offsets: offsets, 52 | CurrentOffset: 0, 53 | }, nil 54 | } 55 | 56 | func (h *Hovers) Read(label string, line []byte) error { 57 | switch label { 58 | case "hoverResult": 59 | if err := h.addData(line); err != nil { 60 | return err 61 | } 62 | case "textDocument/hover": 63 | if err := h.addHoverRef(line); err != nil { 64 | return err 65 | } 66 | case "textDocument/references": 67 | if err := h.addResultSetRef(line); err != nil { 68 | return err 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func (h *Hovers) For(refId Id) json.RawMessage { 76 | var offset Offset 77 | if err := h.Offsets.Entry(refId, &offset); err != nil || offset.Len == 0 { 78 | return nil 79 | } 80 | 81 | hover := make([]byte, offset.Len) 82 | _, err := h.File.ReadAt(hover, int64(offset.At)) 83 | if err != nil { 84 | return nil 85 | } 86 | 87 | return json.RawMessage(hover) 88 | } 89 | 90 | func (h *Hovers) Close() error { 91 | for _, err := range []error{ 92 | h.File.Close(), 93 | h.Offsets.Close(), 94 | } { 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | return nil 100 | } 101 | 102 | func (h *Hovers) addData(line []byte) error { 103 | var rawData RawData 104 | if err := json.Unmarshal(line, &rawData); err != nil { 105 | return err 106 | } 107 | 108 | codeHovers, err := newCodeHovers(rawData.Result.Contents) 109 | if err != nil { 110 | return err 111 | } 112 | 113 | codeHoversData, err := json.Marshal(codeHovers) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | n, err := h.File.Write(codeHoversData) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | offset := Offset{At: int32(h.CurrentOffset), Len: int32(n)} 124 | h.CurrentOffset += n 125 | 126 | return h.Offsets.SetEntry(rawData.Id, &offset) 127 | } 128 | 129 | func (h *Hovers) addHoverRef(line []byte) error { 130 | var hoverRef HoverRef 131 | if err := json.Unmarshal(line, &hoverRef); err != nil { 132 | return err 133 | } 134 | 135 | var offset Offset 136 | if err := h.Offsets.Entry(hoverRef.HoverId, &offset); err != nil { 137 | return err 138 | } 139 | 140 | return h.Offsets.SetEntry(hoverRef.ResultSetId, &offset) 141 | } 142 | 143 | func (h *Hovers) addResultSetRef(line []byte) error { 144 | var ref ResultSetRef 145 | if err := json.Unmarshal(line, &ref); err != nil { 146 | return err 147 | } 148 | 149 | var offset Offset 150 | if err := h.Offsets.Entry(ref.ResultSetId, &offset); err != nil { 151 | return nil 152 | } 153 | 154 | return h.Offsets.SetEntry(ref.RefId, &offset) 155 | } 156 | -------------------------------------------------------------------------------- /graph/file/api_stat.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/williamfzc/srctx/object" 5 | ) 6 | 7 | func (fg *Graph) Stat(f *Vertex) *object.ImpactUnit { 8 | referenceIds := fg.DirectReferenceIds(f) 9 | referencedIds := fg.DirectReferencedIds(f) 10 | 11 | transitiveReferencedIds := fg.TransitiveReferencedIds(f) 12 | transitiveReferenceIds := fg.TransitiveReferenceIds(f) 13 | 14 | entries := fg.EntryIds(f) 15 | 16 | impactUnit := object.NewImpactUnit() 17 | 18 | impactUnit.FileName = f.Path 19 | impactUnit.UnitName = f.Id() 20 | 21 | impactUnit.ImpactCount = len(referencedIds) 22 | impactUnit.TransImpactCount = len(transitiveReferencedIds) 23 | impactUnit.ImpactEntries = len(entries) 24 | 25 | // details 26 | impactUnit.Self = f 27 | impactUnit.ReferenceIds = referenceIds 28 | impactUnit.ReferencedIds = referencedIds 29 | impactUnit.TransitiveReferenceIds = transitiveReferenceIds 30 | impactUnit.TransitiveReferencedIds = transitiveReferencedIds 31 | impactUnit.Entries = entries 32 | 33 | return impactUnit 34 | } 35 | 36 | func (fg *Graph) GlobalStat(points []*Vertex) *object.StatGlobal { 37 | sg := &object.StatGlobal{ 38 | UnitLevel: object.NodeLevelFile, 39 | UnitMapping: make(map[string]int), 40 | } 41 | 42 | // creating mapping 43 | curId := 0 44 | for each := range fg.IdCache { 45 | sg.UnitMapping[each] = curId 46 | curId++ 47 | } 48 | 49 | entries := fg.ListEntries() 50 | totalEntries := make([]int, 0, len(entries)) 51 | for _, each := range entries { 52 | eachId := sg.UnitMapping[each.Id()] 53 | totalEntries = append(totalEntries, eachId) 54 | } 55 | sg.TotalEntries = totalEntries 56 | 57 | stats := make(map[int]*object.ImpactUnit, 0) 58 | for _, each := range points { 59 | eachStat := fg.Stat(each) 60 | eachId := sg.UnitMapping[each.Id()] 61 | stats[eachId] = eachStat 62 | } 63 | sg.ImpactUnitsMap = stats 64 | 65 | // direct impact 66 | directImpactMap := make(map[int]struct{}) 67 | for _, each := range stats { 68 | for _, eachReferenced := range each.ReferencedIds { 69 | eachId := sg.UnitMapping[eachReferenced] 70 | directImpactMap[eachId] = struct{}{} 71 | } 72 | // and itself 73 | itselfId := sg.UnitMapping[each.Self.Id()] 74 | directImpactMap[itselfId] = struct{}{} 75 | } 76 | directImpactList := make([]int, 0, len(directImpactMap)) 77 | for each := range directImpactMap { 78 | directImpactList = append(directImpactList, each) 79 | } 80 | sg.ImpactUnits = directImpactList 81 | 82 | // in-direct impact 83 | indirectImpactMap := make(map[int]struct{}) 84 | for _, each := range stats { 85 | for _, eachReferenced := range each.TransitiveReferencedIds { 86 | eachId := sg.UnitMapping[eachReferenced] 87 | indirectImpactMap[eachId] = struct{}{} 88 | } 89 | // and itself 90 | itselfId := sg.UnitMapping[each.Self.Id()] 91 | indirectImpactMap[itselfId] = struct{}{} 92 | } 93 | indirectImpactList := make([]int, 0, len(indirectImpactMap)) 94 | for each := range indirectImpactMap { 95 | indirectImpactList = append(indirectImpactList, each) 96 | } 97 | sg.TransImpactUnits = indirectImpactList 98 | 99 | // entries 100 | entriesMap := make(map[int]struct{}) 101 | for _, each := range stats { 102 | for _, eachEntry := range each.Entries { 103 | eachId := sg.UnitMapping[eachEntry] 104 | entriesMap[eachId] = struct{}{} 105 | } 106 | } 107 | entriesList := make([]int, 0, len(entriesMap)) 108 | for each := range entriesMap { 109 | entriesList = append(entriesList, each) 110 | } 111 | sg.ImpactEntries = entriesList 112 | 113 | return sg 114 | } 115 | -------------------------------------------------------------------------------- /graph/function/api_storage.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/vmihailenco/msgpack/v5" 7 | ) 8 | 9 | type FgStorage struct { 10 | VertexIds map[int]string `json:"vertexIds"` 11 | GEdges map[int][]int `json:"gEdges"` 12 | RGEdges map[int][]int `json:"rgEdges"` 13 | Cache map[string][]*Vertex `json:"cache"` 14 | } 15 | 16 | func (fg *Graph) DumpFile(fp string) error { 17 | storage, err := fg.Dump() 18 | if err != nil { 19 | return err 20 | } 21 | file, err := os.Create(fp) 22 | if err != nil { 23 | return err 24 | } 25 | defer file.Close() 26 | 27 | encoder := msgpack.NewEncoder(file) 28 | encoder.SetCustomStructTag("json") 29 | if err := encoder.Encode(storage); err != nil { 30 | return err 31 | } 32 | return nil 33 | } 34 | 35 | func (fg *Graph) Dump() (*FgStorage, error) { 36 | ret := &FgStorage{ 37 | VertexIds: make(map[int]string), 38 | GEdges: make(map[int][]int), 39 | RGEdges: make(map[int][]int), 40 | Cache: nil, 41 | } 42 | ret.Cache = fg.Cache 43 | 44 | allVertices := make([]*Vertex, 0) 45 | for _, vertices := range ret.Cache { 46 | for _, each := range vertices { 47 | allVertices = append(allVertices, each) 48 | } 49 | } 50 | reverseMapping := make(map[string]int) 51 | for index, each := range allVertices { 52 | eachId := each.Id() 53 | ret.VertexIds[index] = eachId 54 | reverseMapping[eachId] = index 55 | } 56 | 57 | adjacencyMap, err := fg.g.AdjacencyMap() 58 | if err != nil { 59 | return nil, err 60 | } 61 | for src, v := range adjacencyMap { 62 | for tar := range v { 63 | srcIndex := reverseMapping[src] 64 | tarIndex := reverseMapping[tar] 65 | ret.GEdges[srcIndex] = append(ret.GEdges[srcIndex], tarIndex) 66 | } 67 | } 68 | 69 | // so does rg 70 | adjacencyMap, err = fg.rg.AdjacencyMap() 71 | if err != nil { 72 | return nil, err 73 | } 74 | for src, v := range adjacencyMap { 75 | for tar := range v { 76 | srcIndex := reverseMapping[src] 77 | tarIndex := reverseMapping[tar] 78 | ret.RGEdges[srcIndex] = append(ret.RGEdges[srcIndex], tarIndex) 79 | } 80 | } 81 | 82 | return ret, err 83 | } 84 | 85 | func Load(fgs *FgStorage) (*Graph, error) { 86 | ret := NewEmptyFuncGraph() 87 | 88 | // vertex building 89 | ret.Cache = fgs.Cache 90 | // rebuild id cache 91 | for _, functions := range ret.Cache { 92 | for _, eachFunc := range functions { 93 | ret.IdCache[eachFunc.Id()] = eachFunc 94 | } 95 | } 96 | 97 | for _, eachFile := range ret.Cache { 98 | for _, eachFunc := range eachFile { 99 | _ = ret.g.AddVertex(eachFunc) 100 | _ = ret.rg.AddVertex(eachFunc) 101 | } 102 | } 103 | 104 | // edge building 105 | mapping := fgs.VertexIds 106 | for srcId, targets := range fgs.GEdges { 107 | for _, tarId := range targets { 108 | src := mapping[srcId] 109 | tar := mapping[tarId] 110 | _ = ret.g.AddEdge(src, tar) 111 | } 112 | } 113 | for srcId, targets := range fgs.RGEdges { 114 | for _, tarId := range targets { 115 | src := mapping[srcId] 116 | tar := mapping[tarId] 117 | _ = ret.rg.AddEdge(src, tar) 118 | } 119 | } 120 | return ret, nil 121 | } 122 | 123 | func LoadFile(fp string) (*Graph, error) { 124 | file, err := os.Open(fp) 125 | if err != nil { 126 | return nil, err 127 | } 128 | defer file.Close() 129 | 130 | decoder := msgpack.NewDecoder(file) 131 | storage := &FgStorage{} 132 | decoder.SetCustomStructTag("json") 133 | if err := decoder.Decode(storage); err != nil { 134 | return nil, err 135 | } 136 | 137 | fg, err := Load(storage) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | return fg, nil 143 | } 144 | -------------------------------------------------------------------------------- /cmd/srctx/dump/cmd.go: -------------------------------------------------------------------------------- 1 | package dump 2 | 3 | import ( 4 | "encoding/csv" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/urfave/cli/v2" 7 | "github.com/williamfzc/srctx/object" 8 | "github.com/williamfzc/srctx/parser" 9 | "os" 10 | "slices" 11 | "strconv" 12 | ) 13 | 14 | var flags = []cli.Flag{ 15 | &cli.StringFlag{ 16 | Name: "src", 17 | Value: ".", 18 | Usage: "project path", 19 | }, 20 | &cli.StringFlag{ 21 | Name: "csv", 22 | Value: "output.csv", 23 | Usage: "output csv file", 24 | }, 25 | &cli.StringFlag{ 26 | Name: "lsif", 27 | Value: "", 28 | Usage: "lsif file input", 29 | }, 30 | &cli.StringFlag{ 31 | Name: "scip", 32 | Value: "", 33 | Usage: "scip file input", 34 | }, 35 | } 36 | 37 | type Options struct { 38 | Src string `json:"src"` 39 | Csv string `json:"csv"` 40 | } 41 | 42 | func AddDumpCmd(app *cli.App) { 43 | dumpCmd := &cli.Command{ 44 | Name: "dump", 45 | Usage: "dump file relations", 46 | Flags: flags, 47 | Action: func(cCtx *cli.Context) error { 48 | src := cCtx.String("src") 49 | csvPath := cCtx.String("csv") 50 | lsifPath := cCtx.String("lsif") 51 | scipPath := cCtx.String("scip") 52 | 53 | var sourceContext *object.SourceContext 54 | var err error 55 | if lsifPath == "" && scipPath == "" { 56 | sourceContext, err = parser.FromGolangSrc(src) 57 | if err != nil { 58 | panic(err) 59 | } 60 | } else if lsifPath != "" { 61 | sourceContext, err = parser.FromLsifFile(lsifPath, src) 62 | if err != nil { 63 | panic(err) 64 | } 65 | } else { 66 | sourceContext, err = parser.FromScipFile(scipPath, src) 67 | if err != nil { 68 | panic(err) 69 | } 70 | } 71 | 72 | files := sourceContext.Files() 73 | slices.Sort(files) 74 | log.Infof("files in lsif: %d", len(files)) 75 | 76 | fileCount := len(files) 77 | relationMatrix := make([][]int, fileCount) 78 | for i := range relationMatrix { 79 | relationMatrix[i] = make([]int, fileCount) 80 | } 81 | 82 | fileIndexMap := make(map[string]int) 83 | for idx, file := range files { 84 | fileIndexMap[file] = idx 85 | } 86 | 87 | for _, file := range files { 88 | defs, err := sourceContext.DefsByFileName(file) 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | for _, def := range defs { 94 | refs, err := sourceContext.RefsFromDefId(def.Id()) 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | for _, ref := range refs { 100 | refFile := sourceContext.FileName(ref.FileId) 101 | if refFile == file { 102 | continue 103 | } 104 | if refFile != "" { 105 | relationMatrix[fileIndexMap[file]][fileIndexMap[refFile]]++ 106 | } 107 | } 108 | } 109 | } 110 | 111 | csvFile, err := os.Create(csvPath) 112 | if err != nil { 113 | panic(err) 114 | } 115 | defer csvFile.Close() 116 | 117 | writer := csv.NewWriter(csvFile) 118 | defer writer.Flush() 119 | 120 | header := append([]string{""}, files...) 121 | if err := writer.Write(header); err != nil { 122 | panic(err) 123 | } 124 | 125 | for i, row := range relationMatrix { 126 | csvRow := make([]string, len(row)+1) 127 | csvRow[0] = files[i] 128 | for j, val := range row { 129 | if val == 0 { 130 | csvRow[j+1] = "" 131 | } else { 132 | csvRow[j+1] = strconv.Itoa(val) 133 | } 134 | } 135 | if err := writer.Write(csvRow); err != nil { 136 | panic(err) 137 | } 138 | } 139 | 140 | log.Infof("CSV file generated successfully.") 141 | return nil 142 | }, 143 | } 144 | app.Commands = append(app.Commands, dumpCmd) 145 | } 146 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: SmokeTest 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | tags-ignore: 8 | - '*' 9 | pull_request: 10 | branches: 11 | - '*' 12 | 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | with: 20 | # at least support HEAD~1 for testing 21 | fetch-depth: 2 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v3 25 | with: 26 | go-version: 1.22 27 | 28 | # smoke test 29 | - name: Create index file 30 | run: | 31 | curl -L https://github.com/sourcegraph/lsif-go/releases/download/v1.9.3/src_linux_amd64 -o /usr/local/bin/lsif-go 32 | chmod +x /usr/local/bin/lsif-go 33 | lsif-go -v 34 | 35 | - name: Test 36 | run: go test -v ./... -coverprofile=coverage.txt -coverpkg=./... 37 | 38 | - name: Cmd Test 39 | run: | 40 | make 41 | ./srctx diff --outputHtml output.html --outputJson output.json 42 | cat ./output.json 43 | 44 | - name: Upload coverage to Codecov 45 | uses: codecov/codecov-action@v3 46 | 47 | # smoke test done, start heavy test 48 | - name: Set up Node.js 49 | uses: actions/setup-node@v3 50 | 51 | - name: Build Extra Dep (scip-java) 52 | run: | 53 | # scip-java 54 | curl -fL "https://github.com/coursier/launchers/raw/master/cs-x86_64-pc-linux.gz" | gzip -d > coursier \ 55 | && chmod +x coursier \ 56 | && ./coursier setup -y \ 57 | && ./coursier bootstrap --standalone --bat=true -o scip-java com.sourcegraph:scip-java_2.13:0.8.18 --main com.sourcegraph.scip_java.ScipJava 58 | 59 | - name: Build Extra Dep (lsif-node) 60 | run: | 61 | # lsif-node 62 | npm install -g lsif 63 | lsif -v 64 | 65 | - name: Build Extra Dep (scip-python) 66 | run: | 67 | npm install -g @sourcegraph/scip-python 68 | scip-python -V 69 | 70 | - name: Third Party Test (Golang) 71 | run: | 72 | git clone https://github.com/gin-gonic/gin --depth=6 73 | cd gin 74 | lsif-go -v 75 | ../srctx diff --before HEAD~5 --outputHtml ../golang.html --outputJson ../golang.json 76 | cd .. 77 | cat ./golang.json 78 | 79 | - name: Third Party Test (Java) 80 | run: | 81 | git clone https://github.com/junit-team/junit4 --depth=6 82 | cd junit4 83 | ../scip-java index -- package -DskipTests --batch-mode --errors --settings .github/workflows/settings.xml 84 | ../srctx diff --before HEAD~5 --scip ./index.scip --outputHtml ../java.html --outputJson ../java.json 85 | cd .. 86 | cat ./java.json 87 | 88 | # lsif-node broken 89 | # - name: Thrid Party Test (Node) 90 | # run: | 91 | # git clone https://github.com/microsoft/lsif-node.git --depth=6 92 | # cd lsif-node 93 | # lsif tsc -p ./tsconfig.json --package ./package.json --noContents --out ./dump.lsif 94 | # ../srctx diff --before HEAD~5 --lsif ./dump.lsif --outputHtml ../node.html --outputJson ../node.json 95 | # cd .. 96 | # cat ./node.json 97 | 98 | - name: Thrid Party Test (Python) 99 | run: | 100 | git clone https://github.com/psf/requests.git --depth=6 101 | cd requests 102 | scip-python index . --project-name requests 103 | ../srctx diff --before HEAD~5 --scip ./index.scip --outputHtml ../python.html --outputJson ../python.json 104 | cd .. 105 | cat ./python.json 106 | -------------------------------------------------------------------------------- /cmd/srctx/diff/objects.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/williamfzc/srctx/object" 10 | 11 | "github.com/urfave/cli/v2" 12 | ) 13 | 14 | const ( 15 | // flags 16 | srcFlagName = "src" 17 | repoRootFlagName = "repoRoot" 18 | beforeFlagName = "before" 19 | afterFlagName = "after" 20 | lsifFlagName = "lsif" 21 | scipFlagName = "scip" 22 | nodeLevelFlagName = "nodeLevel" 23 | outputJsonFlagName = "outputJson" 24 | outputCsvFlagName = "outputCsv" 25 | outputDotFlagName = "outputDot" 26 | outputHtmlFlagName = "outputHtml" 27 | withIndexFlagName = "withIndex" 28 | cacheTypeFlagName = "cacheType" 29 | langFlagName = "lang" 30 | noDiffFlagName = "noDiff" 31 | noEntriesFlagName = "noEntries" 32 | indexCmdFlagName = "indexCmd" 33 | statJsonFlagName = "statJson" 34 | 35 | // config file 36 | DefaultConfigFile = "srctx_cfg.json" 37 | ) 38 | 39 | type Options struct { 40 | // required 41 | Src string `json:"src"` 42 | RepoRoot string `json:"repoRoot"` 43 | Before string `json:"before"` 44 | After string `json:"after"` 45 | LsifZip string `json:"lsifZip"` 46 | ScipFile string `json:"scipFile"` 47 | 48 | // output 49 | OutputJson string `json:"outputJson"` 50 | OutputCsv string `json:"outputCsv"` 51 | OutputDot string `json:"outputDot"` 52 | OutputHtml string `json:"outputHtml"` 53 | StatJson string `json:"statJson"` 54 | 55 | // options 56 | NodeLevel string `json:"nodeLevel"` 57 | WithIndex bool `json:"withIndex"` 58 | CacheType string `json:"cacheType"` 59 | Lang string `json:"lang"` 60 | NoDiff bool `json:"noDiff"` 61 | NoEntries bool `json:"noEntries"` 62 | IndexCmd string `json:"indexCmd"` 63 | } 64 | 65 | func NewOptionsFromCliFlags(c *cli.Context) *Options { 66 | return &Options{ 67 | Src: c.String(srcFlagName), 68 | RepoRoot: c.String(repoRootFlagName), 69 | Before: c.String(beforeFlagName), 70 | After: c.String(afterFlagName), 71 | LsifZip: c.String(lsifFlagName), 72 | ScipFile: c.String(scipFlagName), 73 | OutputJson: c.String(outputJsonFlagName), 74 | OutputCsv: c.String(outputCsvFlagName), 75 | OutputDot: c.String(outputDotFlagName), 76 | OutputHtml: c.String(outputHtmlFlagName), 77 | NodeLevel: c.String(nodeLevelFlagName), 78 | WithIndex: c.Bool(withIndexFlagName), 79 | CacheType: c.String(cacheTypeFlagName), 80 | Lang: c.String(langFlagName), 81 | NoDiff: c.Bool(noDiffFlagName), 82 | IndexCmd: c.String(indexCmdFlagName), 83 | StatJson: c.String(statJsonFlagName), 84 | } 85 | } 86 | 87 | func NewOptionsFromSrc(src string) (*Options, error) { 88 | return NewOptionsFromJSONFile(filepath.Join(src, DefaultConfigFile)) 89 | } 90 | 91 | func NewOptionsFromJSONFile(fp string) (*Options, error) { 92 | if _, err := os.Stat(fp); os.IsNotExist(err) { 93 | return nil, fmt.Errorf("config file does not exist at %s", fp) 94 | } 95 | 96 | jsonContent, err := os.ReadFile(fp) 97 | if err != nil { 98 | return nil, fmt.Errorf("failed to read config file: %w", err) 99 | } 100 | 101 | var opts Options 102 | if err := json.Unmarshal(jsonContent, &opts); err != nil { 103 | return nil, fmt.Errorf("failed to parse config: %w", err) 104 | } 105 | return &opts, nil 106 | } 107 | 108 | type ImpactUnitWithFile struct { 109 | *object.ImpactUnit 110 | 111 | // line level impact 112 | ImpactLineCount int `csv:"impactLineCount" json:"impactLineCount"` 113 | TotalLineCount int `csv:"totalLineCount" json:"totalLineCount"` 114 | } 115 | 116 | func WrapImpactUnitWithFile(impactUnit *object.ImpactUnit) *ImpactUnitWithFile { 117 | return &ImpactUnitWithFile{ 118 | ImpactUnit: impactUnit, 119 | ImpactLineCount: 0, 120 | TotalLineCount: 0, 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /parser/lsif/docs.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "archive/zip" 5 | "bufio" 6 | "io" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/goccy/go-json" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const maxScanTokenSize = 1024 * 1024 15 | 16 | type Line struct { 17 | Type string `json:"label"` 18 | } 19 | 20 | type Docs struct { 21 | Root string 22 | Entries map[Id]string 23 | DocRanges map[Id][]Id 24 | Ranges *Ranges 25 | } 26 | 27 | type Document struct { 28 | Id Id `json:"id"` 29 | Uri string `json:"uri"` 30 | } 31 | 32 | type DocumentRange struct { 33 | OutV Id `json:"outV"` 34 | RangeIds []Id `json:"inVs"` 35 | } 36 | 37 | type Metadata struct { 38 | Root string `json:"projectRoot"` 39 | } 40 | 41 | type Source struct { 42 | WorkspaceRoot string `json:"workspaceRoot"` 43 | } 44 | 45 | func NewDocs() (*Docs, error) { 46 | ranges, err := NewRanges() 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return &Docs{ 52 | Root: "file:///", 53 | Entries: make(map[Id]string), 54 | DocRanges: make(map[Id][]Id), 55 | Ranges: ranges, 56 | }, nil 57 | } 58 | 59 | func (d *Docs) Parse(r io.Reader) error { 60 | scanner := bufio.NewScanner(r) 61 | buf := make([]byte, 0, maxScanTokenSize) 62 | scanner.Buffer(buf, 16*maxScanTokenSize) 63 | 64 | for scanner.Scan() { 65 | if err := d.process(scanner.Bytes()); err != nil { 66 | return err 67 | } 68 | } 69 | 70 | return scanner.Err() 71 | } 72 | 73 | func (d *Docs) process(line []byte) error { 74 | l := Line{} 75 | if err := json.Unmarshal(line, &l); err != nil { 76 | if _, ok := err.(*json.SyntaxError); ok { 77 | log.Warnf("invalid json format: %s", string(line)) 78 | return nil 79 | } 80 | 81 | return err 82 | } 83 | 84 | switch l.Type { 85 | case "metaData": 86 | if err := d.addMetadata(line); err != nil { 87 | return err 88 | } 89 | case "source": 90 | // for lsif-node 91 | if err := d.addSource(line); err != nil { 92 | return err 93 | } 94 | case "document": 95 | if err := d.addDocument(line); err != nil { 96 | return err 97 | } 98 | case "contains": 99 | if err := d.addDocRanges(line); err != nil { 100 | return err 101 | } 102 | default: 103 | return d.Ranges.Read(l.Type, line) 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func (d *Docs) Close() error { 110 | return d.Ranges.Close() 111 | } 112 | 113 | func (d *Docs) SerializeEntries(w *zip.Writer) error { 114 | for id, path := range d.Entries { 115 | filePath := Lsif + "/" + path + ".json" 116 | 117 | f, err := w.Create(filePath) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | if err := d.Ranges.Serialize(f, d.DocRanges[id], d.Entries); err != nil { 123 | return err 124 | } 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func (d *Docs) addMetadata(line []byte) error { 131 | var metadata Metadata 132 | if err := json.Unmarshal(line, &metadata); err != nil { 133 | return err 134 | } 135 | 136 | d.Root = strings.TrimSpace(metadata.Root) 137 | 138 | return nil 139 | } 140 | 141 | // lsif stores project info in a split `source` node 142 | func (d *Docs) addSource(line []byte) error { 143 | var source Source 144 | if err := json.Unmarshal(line, &source); err != nil { 145 | return err 146 | } 147 | 148 | d.Root = strings.TrimSpace(source.WorkspaceRoot) 149 | 150 | return nil 151 | } 152 | 153 | func (d *Docs) addDocument(line []byte) error { 154 | var doc Document 155 | if err := json.Unmarshal(line, &doc); err != nil { 156 | return err 157 | } 158 | 159 | relativePath, err := filepath.Rel(d.Root, doc.Uri) 160 | if err != nil { 161 | relativePath = doc.Uri 162 | } 163 | 164 | // for windows 165 | d.Entries[doc.Id] = filepath.ToSlash(relativePath) 166 | 167 | return nil 168 | } 169 | 170 | func (d *Docs) addDocRanges(line []byte) error { 171 | var docRange DocumentRange 172 | if err := json.Unmarshal(line, &docRange); err != nil { 173 | return err 174 | } 175 | 176 | d.DocRanges[docRange.OutV] = append(d.DocRanges[docRange.OutV], docRange.RangeIds...) 177 | 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /cmd/srctx/diff/cmd.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/williamfzc/srctx/object" 9 | 10 | "github.com/goccy/go-json" 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/opensibyl/sibyl2/pkg/core" 14 | "github.com/urfave/cli/v2" 15 | "github.com/williamfzc/srctx/parser/lsif" 16 | ) 17 | 18 | var flags = []cli.Flag{ 19 | &cli.StringFlag{ 20 | Name: srcFlagName, 21 | Value: ".", 22 | Usage: "project path", 23 | }, 24 | &cli.StringFlag{ 25 | Name: repoRootFlagName, 26 | Value: "", 27 | Usage: "root path of your repo", 28 | }, 29 | &cli.StringFlag{ 30 | Name: beforeFlagName, 31 | Value: "HEAD~1", 32 | Usage: "before rev", 33 | }, 34 | &cli.StringFlag{ 35 | Name: afterFlagName, 36 | Value: "HEAD", 37 | Usage: "after rev", 38 | }, 39 | &cli.StringFlag{ 40 | Name: lsifFlagName, 41 | Value: "./dump.lsif", 42 | Usage: "lsif path, can be zip or origin file", 43 | }, 44 | &cli.StringFlag{ 45 | Name: scipFlagName, 46 | Value: "", 47 | Usage: "scip file", 48 | }, 49 | &cli.StringFlag{ 50 | Name: nodeLevelFlagName, 51 | Value: object.NodeLevelFile, 52 | Usage: "graph level (file or func)", 53 | }, 54 | &cli.StringFlag{ 55 | Name: outputJsonFlagName, 56 | Value: "", 57 | Usage: "json output", 58 | }, 59 | &cli.StringFlag{ 60 | Name: outputCsvFlagName, 61 | Value: "", 62 | Usage: "csv output", 63 | }, 64 | &cli.StringFlag{ 65 | Name: outputDotFlagName, 66 | Value: "", 67 | Usage: "reference dot file output", 68 | }, 69 | &cli.StringFlag{ 70 | Name: outputHtmlFlagName, 71 | Value: "", 72 | Usage: "render html report with g6", 73 | }, 74 | &cli.BoolFlag{ 75 | Name: withIndexFlagName, 76 | Value: false, 77 | Usage: "create indexes first if enabled, currently support golang only", 78 | }, 79 | &cli.StringFlag{ 80 | Name: cacheTypeFlagName, 81 | Value: lsif.CacheTypeFile, 82 | Usage: "mem or file", 83 | }, 84 | &cli.StringFlag{ 85 | Name: langFlagName, 86 | Value: string(core.LangUnknown), 87 | Usage: "language of repo", 88 | }, 89 | &cli.BoolFlag{ 90 | Name: noDiffFlagName, 91 | Value: false, 92 | Usage: "will not calc git diff if enabled", 93 | }, 94 | &cli.BoolFlag{ 95 | Name: noEntriesFlagName, 96 | Value: false, 97 | Usage: "will not calc entries if enabled", 98 | }, 99 | &cli.StringFlag{ 100 | Name: indexCmdFlagName, 101 | Value: "", 102 | Usage: "specific scip or lsif cmd", 103 | }, 104 | &cli.StringFlag{ 105 | Name: statJsonFlagName, 106 | Value: "stat.json", 107 | Usage: "", 108 | }, 109 | } 110 | 111 | func AddDiffCmd(app *cli.App) { 112 | diffCmd := &cli.Command{ 113 | Name: "diff", 114 | Usage: "diff with lsif", 115 | Flags: flags, 116 | Action: func(cCtx *cli.Context) error { 117 | opts := NewOptionsFromCliFlags(cCtx) 118 | // standardize the path 119 | src, err := filepath.Abs(opts.Src) 120 | if err != nil { 121 | return err 122 | } 123 | optsFromSrc, err := NewOptionsFromSrc(src) 124 | if err != nil { 125 | // ok 126 | log.Infof("no config: %v", err) 127 | } else { 128 | log.Infof("config file found") 129 | opts = optsFromSrc 130 | } 131 | opts.Src = src 132 | 133 | err = MainDiff(opts) 134 | if err != nil { 135 | return err 136 | } 137 | return nil 138 | }, 139 | } 140 | app.Commands = append(app.Commands, diffCmd) 141 | } 142 | 143 | func AddConfigCmd(app *cli.App) { 144 | configCmd := &cli.Command{ 145 | Name: "diffcfg", 146 | Usage: "create config file for diff", 147 | Flags: flags, 148 | Action: func(cCtx *cli.Context) error { 149 | opts := NewOptionsFromCliFlags(cCtx) 150 | jsonContent, err := json.Marshal(opts) 151 | if err != nil { 152 | return fmt.Errorf("failed to marshal options: %w", err) 153 | } 154 | 155 | configFile := filepath.Join(opts.Src, DefaultConfigFile) 156 | if err := os.WriteFile(configFile, jsonContent, 0o644); err != nil { 157 | return fmt.Errorf("failed to write config file: %w", err) 158 | } 159 | 160 | log.Infof("create config file finished: %v", configFile) 161 | return nil 162 | }, 163 | } 164 | app.Commands = append(app.Commands, configCmd) 165 | } 166 | -------------------------------------------------------------------------------- /parser/lsif/file.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "sync" 7 | ) 8 | 9 | var errInvalid = errors.New("invalid argument") 10 | 11 | // File is an in-memory emulation of the I/O operations of os.File. 12 | // The zero value for File is an empty file ready to use. 13 | // Comes from: https://github.com/dsnet/golib/blob/master/memfile/file.go 14 | type File struct { 15 | m sync.Mutex 16 | b []byte 17 | i int 18 | } 19 | 20 | // New creates and initializes a new File using b as its initial contents. 21 | // The new File takes ownership of b. 22 | func New(b []byte) *File { 23 | return &File{b: b} 24 | } 25 | 26 | // Read reads up to len(b) bytes from the File. 27 | // It returns the number of bytes read and any error encountered. 28 | // At end of file, Read returns (0, io.EOF). 29 | func (fb *File) Read(b []byte) (int, error) { 30 | fb.m.Lock() 31 | defer fb.m.Unlock() 32 | 33 | n, err := fb.readAt(b, int64(fb.i)) 34 | fb.i += n 35 | return n, err 36 | } 37 | 38 | // ReadAt reads len(b) bytes from the File starting at byte offset. 39 | // It returns the number of bytes read and the error, if any. 40 | // At end of file, that error is io.EOF. 41 | func (fb *File) ReadAt(b []byte, offset int64) (int, error) { 42 | fb.m.Lock() 43 | defer fb.m.Unlock() 44 | return fb.readAt(b, offset) 45 | } 46 | 47 | func (fb *File) readAt(b []byte, off int64) (int, error) { 48 | if off < 0 || int64(int(off)) < off { 49 | return 0, errInvalid 50 | } 51 | if off > int64(len(fb.b)) { 52 | return 0, io.EOF 53 | } 54 | n := copy(b, fb.b[off:]) 55 | if n < len(b) { 56 | return n, io.EOF 57 | } 58 | return n, nil 59 | } 60 | 61 | // Write writes len(b) bytes to the File. 62 | // It returns the number of bytes written and an error, if any. 63 | // If the current file offset is past the io.EOF, then the space in-between are 64 | // implicitly filled with zero bytes. 65 | func (fb *File) Write(b []byte) (int, error) { 66 | fb.m.Lock() 67 | defer fb.m.Unlock() 68 | 69 | n, err := fb.writeAt(b, int64(fb.i)) 70 | fb.i += n 71 | return n, err 72 | } 73 | 74 | // WriteAt writes len(b) bytes to the File starting at byte offset. 75 | // It returns the number of bytes written and an error, if any. 76 | // If offset lies past io.EOF, then the space in-between are implicitly filled 77 | // with zero bytes. 78 | func (fb *File) WriteAt(b []byte, offset int64) (int, error) { 79 | fb.m.Lock() 80 | defer fb.m.Unlock() 81 | return fb.writeAt(b, offset) 82 | } 83 | 84 | func (fb *File) writeAt(b []byte, off int64) (int, error) { 85 | if off < 0 || int64(int(off)) < off { 86 | return 0, errInvalid 87 | } 88 | if off > int64(len(fb.b)) { 89 | fb.truncate(off) 90 | } 91 | n := copy(fb.b[off:], b) 92 | fb.b = append(fb.b, b[n:]...) 93 | return len(b), nil 94 | } 95 | 96 | // Seek sets the offset for the next Read or Write on file with offset, 97 | // interpreted according to whence: 0 means relative to the origin of the file, 98 | // 1 means relative to the current offset, and 2 means relative to the end. 99 | func (fb *File) Seek(offset int64, whence int) (int64, error) { 100 | fb.m.Lock() 101 | defer fb.m.Unlock() 102 | 103 | var abs int64 104 | switch whence { 105 | case io.SeekStart: 106 | abs = offset 107 | case io.SeekCurrent: 108 | abs = int64(fb.i) + offset 109 | case io.SeekEnd: 110 | abs = int64(len(fb.b)) + offset 111 | default: 112 | return 0, errInvalid 113 | } 114 | if abs < 0 { 115 | return 0, errInvalid 116 | } 117 | fb.i = int(abs) 118 | return abs, nil 119 | } 120 | 121 | // Truncate changes the size of the file. It does not change the I/O offset. 122 | func (fb *File) Truncate(n int64) error { 123 | fb.m.Lock() 124 | defer fb.m.Unlock() 125 | return fb.truncate(n) 126 | } 127 | 128 | func (fb *File) truncate(n int64) error { 129 | switch { 130 | case n < 0 || int64(int(n)) < n: 131 | return errInvalid 132 | case n <= int64(len(fb.b)): 133 | fb.b = fb.b[:n] 134 | return nil 135 | default: 136 | fb.b = append(fb.b, make([]byte, int(n)-len(fb.b))...) 137 | return nil 138 | } 139 | } 140 | 141 | // Bytes returns the full contents of the File. 142 | // The result in only valid until the next Write, WriteAt, or Truncate call. 143 | func (fb *File) Bytes() []byte { 144 | fb.m.Lock() 145 | defer fb.m.Unlock() 146 | return fb.b 147 | } 148 | -------------------------------------------------------------------------------- /cmd/srctx/diff/core_file.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "os" 7 | 8 | "github.com/williamfzc/srctx/graph/common" 9 | 10 | "github.com/gocarina/gocsv" 11 | "github.com/opensibyl/sibyl2/pkg/core" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/williamfzc/srctx/diff" 14 | "github.com/williamfzc/srctx/graph/file" 15 | ) 16 | 17 | func fileLevelMain(opts *Options, lineMap diff.ImpactLineMap, totalLineCountMap map[string]int) error { 18 | log.Infof("file level main entry") 19 | fileGraph, err := createFileGraph(opts) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | // look up start points 25 | startPoints := make([]*file.Vertex, 0) 26 | for path := range lineMap { 27 | // this vertex may not exist in graph! like: push.yml, README.md 28 | pv := fileGraph.GetById(path) 29 | if pv != nil { 30 | startPoints = append(startPoints, pv) 31 | } 32 | } 33 | // start scan 34 | stats := make([]*ImpactUnitWithFile, 0) 35 | globalStat := fileGraph.GlobalStat(startPoints) 36 | 37 | for _, eachStat := range globalStat.ImpactUnitsMap { 38 | wrappedStat := WrapImpactUnitWithFile(eachStat) 39 | 40 | // fill with file info 41 | if totalLineCount, ok := totalLineCountMap[eachStat.FileName]; ok { 42 | wrappedStat.TotalLineCount = totalLineCount 43 | } 44 | if impactLineCount, ok := lineMap[eachStat.FileName]; ok { 45 | wrappedStat.ImpactLineCount = len(impactLineCount) 46 | } 47 | stats = append(stats, wrappedStat) 48 | log.Infof("start point: %v, refed: %d, ref: %d", eachStat.UnitName, len(eachStat.ReferencedIds), len(eachStat.ReferenceIds)) 49 | } 50 | log.Infof("diff finished.") 51 | 52 | // tag 53 | for _, eachStartPoint := range startPoints { 54 | err = fileGraph.FillWithRed(eachStartPoint.Id()) 55 | if err != nil { 56 | return err 57 | } 58 | } 59 | 60 | if opts.OutputDot != "" { 61 | log.Infof("creating dot file: %v", opts.OutputDot) 62 | 63 | err := fileGraph.DrawDot(opts.OutputDot) 64 | if err != nil { 65 | return err 66 | } 67 | } 68 | 69 | if opts.OutputCsv != "" || opts.OutputJson != "" { 70 | if opts.OutputCsv != "" { 71 | log.Infof("creating output csv: %v", opts.OutputCsv) 72 | csvFile, err := os.OpenFile(opts.OutputCsv, os.O_RDWR|os.O_CREATE, os.ModePerm) 73 | if err != nil { 74 | return err 75 | } 76 | defer csvFile.Close() 77 | if err := gocsv.MarshalFile(&stats, csvFile); err != nil { 78 | return err 79 | } 80 | } 81 | 82 | if opts.OutputJson != "" { 83 | log.Infof("creating output json: %s", opts.OutputJson) 84 | contentBytes, err := json.Marshal(&stats) 85 | if err != nil { 86 | return err 87 | } 88 | err = os.WriteFile(opts.OutputJson, contentBytes, os.ModePerm) 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | } 94 | 95 | if opts.OutputHtml != "" { 96 | log.Infof("creating output html: %s", opts.OutputHtml) 97 | 98 | g6data, err := fileGraph.ToG6Data() 99 | if err != nil { 100 | return err 101 | } 102 | err = g6data.RenderHtml(opts.OutputHtml) 103 | if err != nil { 104 | return err 105 | } 106 | } 107 | 108 | if opts.StatJson != "" { 109 | log.Infof("creating stat json: %s", opts.StatJson) 110 | contentBytes, err := json.Marshal(&globalStat) 111 | if err != nil { 112 | return err 113 | } 114 | err = os.WriteFile(opts.StatJson, contentBytes, os.ModePerm) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func createFileGraph(opts *Options) (*file.Graph, error) { 124 | var fileGraph *file.Graph 125 | var err error 126 | 127 | graphOptions := common.DefaultGraphOptions() 128 | graphOptions.Src = opts.Src 129 | graphOptions.NoEntries = opts.NoEntries 130 | 131 | if opts.ScipFile != "" { 132 | // using SCIP 133 | log.Infof("using SCIP as index") 134 | graphOptions.ScipFile = opts.ScipFile 135 | fileGraph, err = file.CreateFileGraphFromDirWithSCIP(graphOptions) 136 | } else { 137 | // using LSIF 138 | log.Infof("using LSIF as index") 139 | if opts.WithIndex { 140 | switch core.LangType(opts.Lang) { 141 | case core.LangGo: 142 | graphOptions.GenGolangIndex = true 143 | fileGraph, err = file.CreateFileGraphFromGolangDir(graphOptions) 144 | default: 145 | return nil, errors.New("did not specify `--lang`") 146 | } 147 | } else { 148 | graphOptions.LsifFile = opts.LsifZip 149 | fileGraph, err = file.CreateFileGraphFromDirWithLSIF(graphOptions) 150 | } 151 | } 152 | if err != nil { 153 | return nil, err 154 | } 155 | return fileGraph, nil 156 | } 157 | -------------------------------------------------------------------------------- /parser/lsif/code_hover_test.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestHighlight(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | language string 16 | value string 17 | want [][]token 18 | }{ 19 | { 20 | name: "go function definition", 21 | language: "go", 22 | value: "func main()", 23 | want: [][]token{{{Class: "kd", Value: "func"}, {Value: " main()"}}}, 24 | }, 25 | { 26 | name: "go struct definition", 27 | language: "go", 28 | value: "type Command struct", 29 | want: [][]token{{{Class: "kd", Value: "type"}, {Value: " Command "}, {Class: "kd", Value: "struct"}}}, 30 | }, 31 | { 32 | name: "go struct multiline definition", 33 | language: "go", 34 | value: `struct {\nConfig *Config\nReadWriter *ReadWriter\nEOFSent bool\n}`, 35 | want: [][]token{ 36 | {{Class: "kd", Value: "struct"}, {Value: " {\n"}}, 37 | {{Value: "Config *Config\n"}}, 38 | {{Value: "ReadWriter *ReadWriter\n"}}, 39 | {{Value: "EOFSent "}, {Class: "kt", Value: "bool"}, {Value: "\n"}}, 40 | {{Value: "}"}}, 41 | }, 42 | }, 43 | { 44 | name: "ruby method definition", 45 | language: "ruby", 46 | value: "def read(line)", 47 | want: [][]token{{{Class: "k", Value: "def"}, {Value: " read(line)"}}}, 48 | }, 49 | { 50 | name: "ruby multiline method definition", 51 | language: "ruby", 52 | value: `def read(line)\nend`, 53 | want: [][]token{ 54 | {{Class: "k", Value: "def"}, {Value: " read(line)\n"}}, 55 | {{Class: "k", Value: "end"}}, 56 | }, 57 | }, 58 | { 59 | name: "ruby by file extension", 60 | language: "rb", 61 | value: `print hello`, 62 | want: [][]token{ 63 | {{Value: "print hello"}}, 64 | }, 65 | }, 66 | { 67 | name: "unknown/malicious language is passed", 68 | language: " alert(1); ", 69 | value: `def a;\nend`, 70 | want: [][]token(nil), 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | raw := []byte(fmt.Sprintf(`[{"language":"%s","value":"%s"}]`, tt.language, tt.value)) 76 | c, err := newCodeHovers(json.RawMessage(raw)) 77 | 78 | require.NoError(t, err) 79 | require.Len(t, c, 1) 80 | require.Equal(t, tt.want, c[0].Tokens) 81 | }) 82 | } 83 | } 84 | 85 | func TestMarkdown(t *testing.T) { 86 | value := `["This method reverses a string \n\n"]` 87 | c, err := newCodeHovers(json.RawMessage(value)) 88 | 89 | require.NoError(t, err) 90 | require.Len(t, c, 1) 91 | require.Equal(t, "This method reverses a string \n\n", c[0].TruncatedValue.Value) 92 | } 93 | 94 | func TestMarkdownContentsFormat(t *testing.T) { 95 | value := `{"kind":"markdown","value":"some _markdown_ **text**"}` 96 | c, err := newCodeHovers(json.RawMessage(value)) 97 | 98 | require.NoError(t, err) 99 | require.Len(t, c, 1) 100 | require.Equal(t, [][]token(nil), c[0].Tokens) 101 | require.Equal(t, "some _markdown_ **text**", c[0].TruncatedValue.Value) 102 | } 103 | 104 | func TestTruncatedValue(t *testing.T) { 105 | value := strings.Repeat("a", 500) 106 | rawValue, err := json.Marshal(value) 107 | require.NoError(t, err) 108 | 109 | c, err := newCodeHover(rawValue) 110 | require.NoError(t, err) 111 | 112 | require.Equal(t, value[0:maxValueSize], c.TruncatedValue.Value) 113 | require.True(t, c.TruncatedValue.Truncated) 114 | } 115 | 116 | func TestTruncatingMultiByteChars(t *testing.T) { 117 | value := strings.Repeat("ಅ", 500) 118 | rawValue, err := json.Marshal(value) 119 | require.NoError(t, err) 120 | 121 | c, err := newCodeHover(rawValue) 122 | require.NoError(t, err) 123 | 124 | symbolSize := 3 125 | require.Equal(t, value[0:maxValueSize*symbolSize], c.TruncatedValue.Value) 126 | } 127 | 128 | func BenchmarkHighlight(b *testing.B) { 129 | type entry struct { 130 | Language string `json:"language"` 131 | Value string `json:"value"` 132 | } 133 | 134 | tests := []entry{ 135 | { 136 | Language: "go", 137 | Value: "func main()", 138 | }, 139 | { 140 | Language: "ruby", 141 | Value: "def read(line)", 142 | }, 143 | { 144 | Language: "", 145 | Value: "foobar", 146 | }, 147 | { 148 | Language: "zzz", 149 | Value: "def read(line)", 150 | }, 151 | } 152 | 153 | for _, tc := range tests { 154 | b.Run("lang:"+tc.Language, func(b *testing.B) { 155 | raw, err := json.Marshal(tc) 156 | require.NoError(b, err) 157 | 158 | b.ResetTimer() 159 | 160 | for n := 0; n < b.N; n++ { 161 | _, err := newCodeHovers(raw) 162 | require.NoError(b, err) 163 | } 164 | }) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /graph/file/graph.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/dominikbraun/graph" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/williamfzc/srctx/graph/common" 7 | "github.com/williamfzc/srctx/object" 8 | "github.com/williamfzc/srctx/parser" 9 | ) 10 | 11 | const TagEntry = "entry" 12 | 13 | type Graph struct { 14 | // reference graph (called graph) 15 | G graph.Graph[string, *Vertex] 16 | // reverse reference graph (call graph) 17 | Rg graph.Graph[string, *Vertex] 18 | // k: id, v: file 19 | IdCache map[string]*Vertex 20 | } 21 | 22 | func (fg *Graph) ToDirGraph() (*Graph, error) { 23 | // create graph 24 | fileGraph := &Graph{ 25 | G: graph.New((*Vertex).Id, graph.Directed()), 26 | Rg: graph.New((*Vertex).Id, graph.Directed()), 27 | } 28 | 29 | // building edges 30 | err := fileGraph2FileGraph(fg.G, fileGraph.G) 31 | if err != nil { 32 | return nil, err 33 | } 34 | err = fileGraph2FileGraph(fg.Rg, fileGraph.Rg) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | nodeCount, err := fileGraph.G.Order() 40 | if err != nil { 41 | return nil, err 42 | } 43 | edgeCount, err := fileGraph.G.Size() 44 | if err != nil { 45 | return nil, err 46 | } 47 | log.Infof("dir graph ready. nodes: %d, edges: %d", nodeCount, edgeCount) 48 | 49 | return fileGraph, nil 50 | } 51 | 52 | func NewEmptyFileGraph() *Graph { 53 | return &Graph{ 54 | G: graph.New((*Vertex).Id, graph.Directed()), 55 | Rg: graph.New((*Vertex).Id, graph.Directed()), 56 | IdCache: make(map[string]*Vertex), 57 | } 58 | } 59 | 60 | func CreateFileGraphFromDirWithLSIF(opts *common.GraphOptions) (*Graph, error) { 61 | sourceContext, err := parser.FromLsifFile(opts.LsifFile, opts.Src) 62 | if err != nil { 63 | return nil, err 64 | } 65 | log.Infof("fact ready. creating file graph ...") 66 | return CreateFileGraph(sourceContext, opts) 67 | } 68 | 69 | func CreateFileGraphFromGolangDir(opts *common.GraphOptions) (*Graph, error) { 70 | sourceContext, err := parser.FromGolangSrc(opts.Src) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return CreateFileGraph(sourceContext, opts) 75 | } 76 | 77 | func CreateFileGraphFromDirWithSCIP(opts *common.GraphOptions) (*Graph, error) { 78 | sourceContext, err := parser.FromScipFile(opts.ScipFile, opts.Src) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return CreateFileGraph(sourceContext, opts) 83 | } 84 | 85 | func CreateFileGraph(relationship *object.SourceContext, opts *common.GraphOptions) (*Graph, error) { 86 | g := NewEmptyFileGraph() 87 | 88 | // nodes 89 | for each := range relationship.FileMapping { 90 | v := Path2vertex(each) 91 | err := g.G.AddVertex(v) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | err = g.Rg.AddVertex(v) 97 | if err != nil { 98 | return nil, err 99 | } 100 | 101 | g.IdCache[each] = v 102 | } 103 | 104 | current := 0 105 | total := len(relationship.FileMapping) 106 | for eachSrcFile := range relationship.FileMapping { 107 | log.Infof("processing: %d / %d", current, total) 108 | current++ 109 | 110 | refs, err := relationship.RefsByFileName(eachSrcFile) 111 | if err != nil { 112 | return nil, err 113 | } 114 | for _, eachRef := range refs { 115 | defs, err := relationship.RefsFromDefId(eachRef.Id()) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | for _, eachDef := range defs { 121 | targetFile := relationship.FileName(eachDef.FileId) 122 | 123 | // avoid itself 124 | if eachSrcFile == targetFile { 125 | continue 126 | } 127 | 128 | refLineNumber := eachRef.LineNumber() 129 | if edge, err := g.G.Edge(eachSrcFile, targetFile); err == nil { 130 | storage := edge.Properties.Data.(*common.EdgeStorage) 131 | storage.RefLines[refLineNumber] = struct{}{} 132 | } else { 133 | _ = g.G.AddEdge(eachSrcFile, targetFile, graph.EdgeData(common.NewEdgeStorage())) 134 | } 135 | 136 | if edge, err := g.Rg.Edge(targetFile, eachSrcFile); err == nil { 137 | storage := edge.Properties.Data.(*common.EdgeStorage) 138 | storage.RefLines[refLineNumber] = struct{}{} 139 | } else { 140 | _ = g.Rg.AddEdge(targetFile, eachSrcFile, graph.EdgeData(common.NewEdgeStorage())) 141 | } 142 | } 143 | } 144 | } 145 | 146 | // entries tag 147 | // calculation takes a few minutes if target project is large 148 | if !opts.NoEntries { 149 | entries := g.FilterFunctions(func(vertex *Vertex) bool { 150 | return len(g.DirectReferencedIds(vertex)) == 0 151 | }) 152 | log.Infof("detect entries: %d", len(entries)) 153 | for _, entry := range entries { 154 | entry.AddTag(TagEntry) 155 | } 156 | } 157 | 158 | nodeCount, err := g.G.Order() 159 | if err != nil { 160 | return nil, err 161 | } 162 | edgeCount, err := g.G.Size() 163 | if err != nil { 164 | return nil, err 165 | } 166 | log.Infof("file graph ready. nodes: %d, edges: %d", nodeCount, edgeCount) 167 | 168 | return g, nil 169 | } 170 | -------------------------------------------------------------------------------- /parser/lsif/code_hover.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "unicode/utf8" 7 | 8 | "github.com/alecthomas/chroma/v2" 9 | "github.com/alecthomas/chroma/v2/lexers" 10 | ) 11 | 12 | const maxValueSize = 250 13 | 14 | type token struct { 15 | Class string `json:"class,omitempty"` 16 | Value string `json:"value"` 17 | } 18 | 19 | type codeHover struct { 20 | TruncatedValue *truncatableString `json:"value,omitempty"` 21 | Tokens [][]token `json:"tokens,omitempty"` 22 | Language string `json:"language,omitempty"` 23 | Truncated bool `json:"truncated,omitempty"` 24 | } 25 | 26 | type truncatableString struct { 27 | Value string 28 | Truncated bool 29 | } 30 | 31 | // supportedLexerLanguages is used for a fast lookup to ensure the language 32 | // is supported by the lexer library. 33 | var supportedLexerLanguages = map[string]struct{}{} 34 | 35 | func init() { 36 | for _, name := range lexers.Names(true) { 37 | supportedLexerLanguages[name] = struct{}{} 38 | } 39 | } 40 | 41 | func (ts *truncatableString) UnmarshalText(b []byte) error { 42 | s := 0 43 | for i := 0; s < len(b); i++ { 44 | if i >= maxValueSize { 45 | ts.Truncated = true 46 | break 47 | } 48 | 49 | _, size := utf8.DecodeRune(b[s:]) 50 | 51 | s += size 52 | } 53 | 54 | ts.Value = string(b[0:s]) 55 | 56 | return nil 57 | } 58 | 59 | func (ts *truncatableString) MarshalJSON() ([]byte, error) { 60 | return json.Marshal(ts.Value) 61 | } 62 | 63 | func newCodeHovers(contents json.RawMessage) ([]*codeHover, error) { 64 | var rawContents []json.RawMessage 65 | if err := json.Unmarshal(contents, &rawContents); err != nil { 66 | rawContents = []json.RawMessage{contents} 67 | } 68 | 69 | codeHovers := []*codeHover{} 70 | for _, rawContent := range rawContents { 71 | c, err := newCodeHover(rawContent) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | codeHovers = append(codeHovers, c) 77 | } 78 | 79 | return codeHovers, nil 80 | } 81 | 82 | func newCodeHover(content json.RawMessage) (*codeHover, error) { 83 | // Hover value can be either an object: { "value": "func main()", "language": "go" } 84 | // Or a string with documentation 85 | // Or a markdown object: { "value": "```go\nfunc main()\n```", "kind": "markdown" } 86 | // We try to unmarshal the content into a string and if we fail, we unmarshal it into an object 87 | var c codeHover 88 | if err := json.Unmarshal(content, &c.TruncatedValue); err != nil { 89 | if err := json.Unmarshal(content, &c); err != nil { 90 | return nil, err 91 | } 92 | 93 | c.setTokens() 94 | } 95 | 96 | c.Truncated = c.TruncatedValue.Truncated 97 | 98 | if len(c.Tokens) > 0 { 99 | c.TruncatedValue = nil // remove value for hovers which have tokens 100 | } 101 | 102 | return &c, nil 103 | } 104 | 105 | func (c *codeHover) setTokens() { 106 | // fastpath: bail early if no language specified 107 | if c.Language == "" { 108 | return 109 | } 110 | 111 | // fastpath: lexer.Get() will first match against indexed languages by 112 | // name and alias, and then fallback to a very slow filepath match. We 113 | // avoid this slow path by first checking against languages we know to 114 | // be within the index, and bailing if not found. 115 | // 116 | // Not case-folding immediately is done intentionally. These two lookups 117 | // mirror the behaviour of lexer.Get(). 118 | if _, ok := supportedLexerLanguages[c.Language]; !ok { 119 | if _, ok := supportedLexerLanguages[strings.ToLower(c.Language)]; !ok { 120 | return 121 | } 122 | } 123 | 124 | lexer := lexers.Get(c.Language) 125 | if lexer == nil { 126 | return 127 | } 128 | 129 | iterator, err := lexer.Tokenise(nil, c.TruncatedValue.Value) 130 | if err != nil { 131 | return 132 | } 133 | 134 | var tokenLines [][]token 135 | for _, tokenLine := range chroma.SplitTokensIntoLines(iterator.Tokens()) { 136 | var tokens []token 137 | var rawToken string 138 | for _, t := range tokenLine { 139 | class := c.classFor(t.Type) 140 | 141 | // accumulate consequent raw values in a single string to store them as 142 | // [{ Class: "kd", Value: "func" }, { Value: " main() {" }] instead of 143 | // [{ Class: "kd", Value: "func" }, { Value: " " }, { Value: "main" }, { Value: "(" }...] 144 | if class == "" { 145 | rawToken = rawToken + t.Value 146 | } else { 147 | if rawToken != "" { 148 | tokens = append(tokens, token{Value: rawToken}) 149 | rawToken = "" 150 | } 151 | 152 | tokens = append(tokens, token{Class: class, Value: t.Value}) 153 | } 154 | } 155 | 156 | if rawToken != "" { 157 | tokens = append(tokens, token{Value: rawToken}) 158 | } 159 | 160 | tokenLines = append(tokenLines, tokens) 161 | } 162 | 163 | c.Tokens = tokenLines 164 | } 165 | 166 | func (c *codeHover) classFor(tokenType chroma.TokenType) string { 167 | if strings.HasPrefix(tokenType.String(), "Keyword") || tokenType == chroma.String || tokenType == chroma.Comment { 168 | return chroma.StandardTypes[tokenType] 169 | } 170 | 171 | return "" 172 | } 173 | -------------------------------------------------------------------------------- /cmd/srctx/diff/core_func.go: -------------------------------------------------------------------------------- 1 | package diff 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "os" 7 | 8 | "github.com/gocarina/gocsv" 9 | "github.com/opensibyl/sibyl2/pkg/core" 10 | log "github.com/sirupsen/logrus" 11 | "github.com/williamfzc/srctx/diff" 12 | "github.com/williamfzc/srctx/graph/function" 13 | "github.com/williamfzc/srctx/graph/visual/g6" 14 | ) 15 | 16 | func funcLevelMain(opts *Options, lineMap diff.ImpactLineMap, totalLineCountMap map[string]int) error { 17 | log.Infof("func level main entry") 18 | // metadata 19 | funcGraph, err := createFuncGraph(opts) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | // look up start points 25 | startPoints := make([]*function.Vertex, 0) 26 | for path, lines := range lineMap { 27 | curPoints := funcGraph.GetFunctionsByFileLines(path, lines) 28 | if len(curPoints) == 0 { 29 | log.Infof("file %s line %v hit no func", path, lines) 30 | } else { 31 | startPoints = append(startPoints, curPoints...) 32 | } 33 | } 34 | 35 | // start scan 36 | stats := make([]*ImpactUnitWithFile, 0) 37 | globalStat := funcGraph.GlobalStat(startPoints) 38 | 39 | for _, eachStat := range globalStat.ImpactUnitsMap { 40 | wrappedStat := WrapImpactUnitWithFile(eachStat) 41 | self := eachStat.Self.(*function.Vertex) 42 | 43 | totalLineCount := len(self.GetSpan().Lines()) 44 | impactLineCount := 0 45 | 46 | if lines, ok := lineMap[self.Path]; ok { 47 | for _, eachLine := range lines { 48 | if self.GetSpan().ContainLine(eachLine) { 49 | impactLineCount++ 50 | } 51 | } 52 | } 53 | wrappedStat.TotalLineCount = totalLineCount 54 | wrappedStat.ImpactLineCount = impactLineCount 55 | 56 | stats = append(stats, wrappedStat) 57 | log.Infof("start point: %v, refed: %d, ref: %d", self.Id(), len(eachStat.ReferencedIds), len(eachStat.ReferenceIds)) 58 | } 59 | log.Infof("diff finished.") 60 | 61 | // tag (with priority) 62 | for _, eachStat := range stats { 63 | for _, eachVisited := range eachStat.VisitedIds() { 64 | err := funcGraph.FillWithYellow(eachVisited) 65 | if err != nil { 66 | return err 67 | } 68 | } 69 | } 70 | for _, eachStat := range stats { 71 | for _, eachId := range eachStat.ReferencedIds { 72 | err := funcGraph.FillWithOrange(eachId) 73 | if err != nil { 74 | return err 75 | } 76 | } 77 | } 78 | for _, eachStat := range stats { 79 | err := funcGraph.FillWithRed(eachStat.UnitName) 80 | if err != nil { 81 | return err 82 | } 83 | } 84 | 85 | // output 86 | if opts.OutputDot != "" { 87 | log.Infof("creating dot file: %v", opts.OutputDot) 88 | 89 | err := funcGraph.DrawDot(opts.OutputDot) 90 | if err != nil { 91 | return err 92 | } 93 | } 94 | 95 | if opts.OutputCsv != "" || opts.OutputJson != "" { 96 | if opts.OutputCsv != "" { 97 | log.Infof("creating output csv: %v", opts.OutputCsv) 98 | csvFile, err := os.OpenFile(opts.OutputCsv, os.O_RDWR|os.O_CREATE, os.ModePerm) 99 | if err != nil { 100 | return err 101 | } 102 | defer csvFile.Close() 103 | if err := gocsv.MarshalFile(&stats, csvFile); err != nil { 104 | return err 105 | } 106 | } 107 | 108 | if opts.OutputJson != "" { 109 | log.Infof("creating output json: %s", opts.OutputJson) 110 | contentBytes, err := json.Marshal(&stats) 111 | if err != nil { 112 | return err 113 | } 114 | err = os.WriteFile(opts.OutputJson, contentBytes, os.ModePerm) 115 | if err != nil { 116 | return err 117 | } 118 | } 119 | } 120 | 121 | if opts.OutputHtml != "" { 122 | log.Infof("creating output html: %s", opts.OutputHtml) 123 | 124 | var g6data *g6.Data 125 | g6data, err = funcGraph.ToG6Data() 126 | if err != nil { 127 | return err 128 | } 129 | 130 | err = g6data.RenderHtml(opts.OutputHtml) 131 | if err != nil { 132 | return err 133 | } 134 | } 135 | 136 | if opts.StatJson != "" { 137 | log.Infof("creating stat json: %s", opts.StatJson) 138 | contentBytes, err := json.Marshal(&globalStat) 139 | if err != nil { 140 | return err 141 | } 142 | err = os.WriteFile(opts.StatJson, contentBytes, os.ModePerm) 143 | if err != nil { 144 | return err 145 | } 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func createFuncGraph(opts *Options) (*function.Graph, error) { 152 | var funcGraph *function.Graph 153 | var err error 154 | 155 | if opts.ScipFile != "" { 156 | // using SCIP 157 | log.Infof("using SCIP as index") 158 | funcGraph, err = function.CreateFuncGraphFromDirWithSCIP(opts.Src, opts.ScipFile, core.LangType(opts.Lang)) 159 | } else { 160 | // using LSIF 161 | log.Infof("using LSIF as index") 162 | if opts.WithIndex { 163 | switch core.LangType(opts.Lang) { 164 | case core.LangGo: 165 | funcGraph, err = function.CreateFuncGraphFromGolangDir(opts.Src) 166 | default: 167 | return nil, errors.New("did not specify `--lang`") 168 | } 169 | } else { 170 | funcGraph, err = function.CreateFuncGraphFromDirWithLSIF(opts.Src, opts.LsifZip, core.LangType(opts.Lang)) 171 | } 172 | } 173 | if err != nil { 174 | return nil, err 175 | } 176 | return funcGraph, nil 177 | } 178 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/williamfzc/srctx 2 | 3 | go 1.22.0 4 | 5 | toolchain go1.22.9 6 | 7 | require ( 8 | github.com/alecthomas/chroma/v2 v2.7.0 9 | github.com/bluekeyes/go-gitdiff v0.7.1 10 | github.com/cockroachdb/errors v1.11.1 11 | github.com/dominikbraun/graph v0.22.0 12 | github.com/gocarina/gocsv v0.0.0-20230406101422-6445c2b15027 13 | github.com/goccy/go-json v0.10.2 14 | github.com/opensibyl/sibyl2 v0.16.4 15 | github.com/sirupsen/logrus v1.9.0 16 | github.com/sourcegraph/lsif-go v1.9.3 17 | github.com/sourcegraph/scip v0.3.1-0.20230627154934-45df7f6d33fc 18 | github.com/stretchr/testify v1.8.4 19 | github.com/urfave/cli/v2 v2.25.1 20 | github.com/vmihailenco/msgpack/v5 v5.3.5 21 | gonum.org/v1/gonum v0.14.0 22 | google.golang.org/protobuf v1.34.1 23 | ) 24 | 25 | require ( 26 | github.com/Masterminds/goutils v1.1.1 // indirect 27 | github.com/Masterminds/semver v1.5.0 // indirect 28 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 29 | github.com/agnivade/levenshtein v1.1.1 // indirect 30 | github.com/alecthomas/kingpin v2.2.6+incompatible // indirect 31 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 32 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 33 | github.com/bufbuild/buf v1.4.0 // indirect 34 | github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect 35 | github.com/cockroachdb/redact v1.1.5 // indirect 36 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 37 | github.com/davecgh/go-spew v1.1.1 // indirect 38 | github.com/dlclark/regexp2 v1.8.1 // indirect 39 | github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101 // indirect 40 | github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect 41 | github.com/getsentry/sentry-go v0.25.0 // indirect 42 | github.com/gofrs/flock v0.8.1 // indirect 43 | github.com/gofrs/uuid v4.2.0+incompatible // indirect 44 | github.com/gogo/protobuf v1.3.2 // indirect 45 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 46 | github.com/golang/protobuf v1.5.4 // indirect 47 | github.com/google/uuid v1.6.0 // indirect 48 | github.com/huandu/xstrings v1.3.2 // indirect 49 | github.com/imdario/mergo v0.3.13 // indirect 50 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 51 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 52 | github.com/jackc/pgconn v1.14.3 // indirect 53 | github.com/jackc/pgio v1.0.0 // indirect 54 | github.com/jackc/pgpassfile v1.0.0 // indirect 55 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 56 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 57 | github.com/jdxcode/netrc v0.0.0-20210204082910-926c7f70242a // indirect 58 | github.com/jhump/protocompile v0.0.0-20220216033700-d705409f108f // indirect 59 | github.com/jhump/protoreflect v1.12.1-0.20220417024638-438db461d753 // indirect 60 | github.com/json-iterator/go v1.1.12 // indirect 61 | github.com/klauspost/compress v1.16.0 // indirect 62 | github.com/klauspost/pgzip v1.2.5 // indirect 63 | github.com/kr/pretty v0.3.1 // indirect 64 | github.com/kr/text v0.2.0 // indirect 65 | github.com/mitchellh/copystructure v1.2.0 // indirect 66 | github.com/mitchellh/mapstructure v1.5.0 // indirect 67 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 69 | github.com/modern-go/reflect2 v1.0.2 // indirect 70 | github.com/mwitkow/go-proto-validators v0.3.2 // indirect 71 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect 72 | github.com/pkg/errors v0.9.1 // indirect 73 | github.com/pkg/profile v1.6.0 // indirect 74 | github.com/pmezard/go-difflib v1.0.0 // indirect 75 | github.com/pseudomuto/protoc-gen-doc v1.5.1 // indirect 76 | github.com/pseudomuto/protokit v0.2.0 // indirect 77 | github.com/rogpeppe/go-internal v1.11.0 // indirect 78 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 79 | github.com/slimsag/godocmd v0.0.0-20161025000126-a1005ad29fe3 // indirect 80 | github.com/smacker/go-tree-sitter v0.0.0-20230501083651-a7d92773b3aa // indirect 81 | github.com/sourcegraph/sourcegraph/lib v0.0.0-20220511160847-5a43d3ea24eb // indirect 82 | github.com/spf13/cobra v1.5.0 // indirect 83 | github.com/spf13/pflag v1.0.5 // indirect 84 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 85 | github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect 86 | go.opencensus.io v0.24.0 // indirect 87 | go.uber.org/atomic v1.11.0 // indirect 88 | go.uber.org/goleak v1.2.1 // indirect 89 | go.uber.org/multierr v1.11.0 // indirect 90 | go.uber.org/zap v1.24.0 // indirect 91 | golang.org/x/crypto v0.29.0 // indirect 92 | golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect 93 | golang.org/x/mod v0.22.0 // indirect 94 | golang.org/x/net v0.31.0 // indirect 95 | golang.org/x/sync v0.9.0 // indirect 96 | golang.org/x/sys v0.27.0 // indirect 97 | golang.org/x/term v0.26.0 // indirect 98 | golang.org/x/text v0.20.0 // indirect 99 | golang.org/x/tools v0.27.0 // indirect 100 | golang.org/x/tools/go/vcs v0.1.0-deprecated // indirect 101 | google.golang.org/genproto v0.0.0-20231030173426-d783a09b4405 // indirect 102 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect 103 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 104 | google.golang.org/grpc v1.65.0 // indirect 105 | gopkg.in/yaml.v2 v2.4.0 // indirect 106 | gopkg.in/yaml.v3 v3.0.1 // indirect 107 | ) 108 | 109 | // lsif-go 110 | replace github.com/sourcegraph/lsif-go => github.com/williamfzc/lsif-go v0.0.0-20241123083816-a7f1cc66418e 111 | 112 | replace mvdan.cc/gofumpt => github.com/mvdan/gofumpt v0.5.0 113 | 114 | // scip 115 | replace github.com/sourcegraph/scip => github.com/williamfzc/scip v0.0.0-20230518120517-4d9044d8f05b 116 | 117 | // private repo now 118 | replace github.com/sourcegraph/sourcegraph/lib => github.com/sourcegraph/sourcegraph-public-snapshot/lib v0.0.0-20240822153003-c864f15af264 119 | -------------------------------------------------------------------------------- /graph/function/graph.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/williamfzc/srctx/graph/common" 9 | 10 | "github.com/dominikbraun/graph" 11 | "github.com/opensibyl/sibyl2/pkg/core" 12 | log "github.com/sirupsen/logrus" 13 | "github.com/williamfzc/srctx/object" 14 | "github.com/williamfzc/srctx/parser" 15 | ) 16 | 17 | const ( 18 | TagEntry = "entry" 19 | ) 20 | 21 | type FuncPos struct { 22 | Path string `json:"path,omitempty"` 23 | Lang string `json:"lang,omitempty"` 24 | Start int `json:"start,omitempty"` 25 | End int `json:"end,omitempty"` 26 | } 27 | 28 | func (f *FuncPos) Repr() string { 29 | return fmt.Sprintf("%s#%d-%d", f.Path, f.Start, f.End) 30 | } 31 | 32 | type Graph struct { 33 | // reference graph (called graph), ref -> def 34 | g graph.Graph[string, *Vertex] 35 | // reverse reference graph (call graph), def -> ref 36 | rg graph.Graph[string, *Vertex] 37 | 38 | // k: file, v: function 39 | Cache map[string][]*Vertex 40 | // k: id, v: function 41 | IdCache map[string]*Vertex 42 | 43 | // source context ptr 44 | sc *object.SourceContext 45 | } 46 | 47 | func NewEmptyFuncGraph() *Graph { 48 | return &Graph{ 49 | g: graph.New((*Vertex).Id, graph.Directed()), 50 | rg: graph.New((*Vertex).Id, graph.Directed()), 51 | Cache: make(map[string][]*Vertex), 52 | IdCache: make(map[string]*Vertex), 53 | sc: nil, 54 | } 55 | } 56 | 57 | func CreateFuncGraph(fact *FactStorage, relationship *object.SourceContext) (*Graph, error) { 58 | fg := NewEmptyFuncGraph() 59 | 60 | // add all the nodes 61 | for path, file := range fact.cache { 62 | for _, eachFunc := range file.Units { 63 | cur := CreateFuncVertex(eachFunc, file) 64 | _ = fg.g.AddVertex(cur) 65 | fg.Cache[path] = append(fg.Cache[path], cur) 66 | fg.IdCache[cur.Id()] = cur 67 | } 68 | } 69 | 70 | // also reverse graph 71 | rg, err := fg.g.Clone() 72 | if err != nil { 73 | return nil, err 74 | } 75 | fg.rg = rg 76 | 77 | // building edges 78 | log.Infof("edges building") 79 | for path, funcs := range fg.Cache { 80 | for _, eachFunc := range funcs { 81 | // there are multi defs happened in this line 82 | refs, err := relationship.RefsFromLineWithLimit(path, eachFunc.DefLine, len(eachFunc.Name)) 83 | log.Debugf("search from %s#%d, ref: %d", path, eachFunc.DefLine, len(refs)) 84 | if err != nil { 85 | // no refs 86 | continue 87 | } 88 | for _, eachRef := range refs { 89 | refFile := relationship.FileName(eachRef.FileId) 90 | refFile = filepath.ToSlash(filepath.Clean(refFile)) 91 | 92 | isFuncRef := false 93 | symbols := fact.GetSymbolsByFileAndLine(refFile, eachRef.IndexLineNumber()) 94 | for _, eachSymbol := range symbols { 95 | if eachSymbol.Unit.Content == eachFunc.Name { 96 | isFuncRef = true 97 | break 98 | } 99 | } 100 | if !isFuncRef { 101 | continue 102 | } 103 | 104 | for _, eachPossibleFunc := range fg.Cache[refFile] { 105 | // eachPossibleFunc 's range contains eachFunc 's ref 106 | // so eachPossibleFunc calls eachFunc 107 | if eachPossibleFunc.GetSpan().ContainLine(eachRef.IndexLineNumber()) { 108 | refLineNumber := eachRef.LineNumber() 109 | log.Debugf("%v refed in %s#%v", eachFunc.Id(), refFile, refLineNumber) 110 | 111 | if edge, err := fg.g.Edge(eachFunc.Id(), eachPossibleFunc.Id()); err == nil { 112 | storage := edge.Properties.Data.(*common.EdgeStorage) 113 | storage.RefLines[refLineNumber] = struct{}{} 114 | } else { 115 | _ = fg.g.AddEdge(eachFunc.Id(), eachPossibleFunc.Id(), graph.EdgeData(common.NewEdgeStorage())) 116 | } 117 | 118 | if edge, err := fg.rg.Edge(eachPossibleFunc.Id(), eachFunc.Id()); err == nil { 119 | storage := edge.Properties.Data.(*common.EdgeStorage) 120 | storage.RefLines[refLineNumber] = struct{}{} 121 | } else { 122 | _ = fg.rg.AddEdge(eachPossibleFunc.Id(), eachFunc.Id(), graph.EdgeData(common.NewEdgeStorage())) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | // entries tag 131 | entries := fg.FilterFunctions(func(funcVertex *Vertex) bool { 132 | return len(fg.DirectReferencedIds(funcVertex)) == 0 133 | }) 134 | log.Infof("detect entries: %d", len(entries)) 135 | for _, entry := range entries { 136 | entry.AddTag(TagEntry) 137 | } 138 | 139 | nodeCount, err := fg.g.Order() 140 | if err != nil { 141 | return nil, err 142 | } 143 | edgeCount, err := fg.g.Size() 144 | if err != nil { 145 | return nil, err 146 | } 147 | log.Infof("func graph ready. nodes: %d, edges: %d", nodeCount, edgeCount) 148 | 149 | return fg, nil 150 | } 151 | 152 | func CreateFuncGraphFromGolangDir(src string) (*Graph, error) { 153 | sourceContext, err := parser.FromGolangSrc(src) 154 | if err != nil { 155 | return nil, err 156 | } 157 | return srcctx2graph(src, sourceContext, core.LangGo) 158 | } 159 | 160 | func CreateFuncGraphFromDirWithLSIF(src string, lsifFile string, lang core.LangType) (*Graph, error) { 161 | sourceContext, err := parser.FromLsifFile(lsifFile, src) 162 | if err != nil { 163 | return nil, err 164 | } 165 | return srcctx2graph(src, sourceContext, lang) 166 | } 167 | 168 | func CreateFuncGraphFromDirWithSCIP(src string, scipFile string, lang core.LangType) (*Graph, error) { 169 | sourceContext, err := parser.FromScipFile(scipFile, src) 170 | if err != nil { 171 | return nil, err 172 | } 173 | return srcctx2graph(src, sourceContext, lang) 174 | } 175 | 176 | func srcctx2graph(src string, sourceContext *object.SourceContext, lang core.LangType) (*Graph, error) { 177 | log.Infof("createing fact with sibyl2") 178 | 179 | // change workdir because sibyl2 needs to access the files 180 | originWorkdir, err := os.Getwd() 181 | if err != nil { 182 | return nil, err 183 | } 184 | err = os.Chdir(src) 185 | if err != nil { 186 | return nil, err 187 | } 188 | defer func() { 189 | _ = os.Chdir(originWorkdir) 190 | }() 191 | 192 | factStorage, err := CreateFact(src, lang) 193 | if err != nil { 194 | return nil, err 195 | } 196 | log.Infof("fact ready. creating func graph ...") 197 | funcGraph, err := CreateFuncGraph(factStorage, sourceContext) 198 | if err != nil { 199 | return nil, err 200 | } 201 | return funcGraph, nil 202 | } 203 | -------------------------------------------------------------------------------- /parser/lsif/testdata/expected/lsif/main.go.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "start_line": 7, 4 | "start_char": 1, 5 | "definition_path": "main.go#L4", 6 | "hover": [ 7 | { 8 | "tokens": [ 9 | [ 10 | { 11 | "class": "kn", 12 | "value": "package" 13 | }, 14 | { 15 | "value": " " 16 | }, 17 | { 18 | "class": "s", 19 | "value": "\"github.com/user/hello/morestrings\"" 20 | } 21 | ] 22 | ], 23 | "language": "go" 24 | }, 25 | { 26 | "value": "Package morestrings implements additional functions to manipulate UTF-8 encoded strings, beyond what is provided in the standard \"strings\" package. \n\n" 27 | } 28 | ], 29 | "references": [ 30 | { 31 | "path": "main.go#L8" 32 | }, 33 | { 34 | "path": "main.go#L9" 35 | } 36 | ] 37 | }, 38 | { 39 | "start_line": 7, 40 | "start_char": 13, 41 | "definition_path": "morestrings/reverse.go#L12", 42 | "hover": [ 43 | { 44 | "tokens": [ 45 | [ 46 | { 47 | "class": "kd", 48 | "value": "func" 49 | }, 50 | { 51 | "value": " Reverse(s " 52 | }, 53 | { 54 | "class": "kt", 55 | "value": "string" 56 | }, 57 | { 58 | "value": ") " 59 | }, 60 | { 61 | "class": "kt", 62 | "value": "string" 63 | } 64 | ] 65 | ], 66 | "language": "go" 67 | }, 68 | { 69 | "value": "This method reverses a string \n\n" 70 | } 71 | ], 72 | "references": [ 73 | { 74 | "path": "main.go#L8" 75 | } 76 | ] 77 | }, 78 | { 79 | "start_line": 8, 80 | "start_char": 1, 81 | "definition_path": "main.go#L4", 82 | "hover": [ 83 | { 84 | "tokens": [ 85 | [ 86 | { 87 | "class": "kn", 88 | "value": "package" 89 | }, 90 | { 91 | "value": " " 92 | }, 93 | { 94 | "class": "s", 95 | "value": "\"github.com/user/hello/morestrings\"" 96 | } 97 | ] 98 | ], 99 | "language": "go" 100 | }, 101 | { 102 | "value": "Package morestrings implements additional functions to manipulate UTF-8 encoded strings, beyond what is provided in the standard \"strings\" package. \n\n" 103 | } 104 | ], 105 | "references": [ 106 | { 107 | "path": "main.go#L8" 108 | }, 109 | { 110 | "path": "main.go#L9" 111 | } 112 | ] 113 | }, 114 | { 115 | "start_line": 8, 116 | "start_char": 13, 117 | "definition_path": "morestrings/reverse.go#L5", 118 | "hover": [ 119 | { 120 | "tokens": [ 121 | [ 122 | { 123 | "class": "kd", 124 | "value": "func" 125 | }, 126 | { 127 | "value": " Func2(i " 128 | }, 129 | { 130 | "class": "kt", 131 | "value": "int" 132 | }, 133 | { 134 | "value": ") " 135 | }, 136 | { 137 | "class": "kt", 138 | "value": "string" 139 | } 140 | ] 141 | ], 142 | "language": "go" 143 | } 144 | ], 145 | "references": [ 146 | { 147 | "path": "main.go#L9" 148 | } 149 | ] 150 | }, 151 | { 152 | "start_line": 6, 153 | "start_char": 5, 154 | "definition_path": "main.go#L7", 155 | "hover": [ 156 | { 157 | "tokens": [ 158 | [ 159 | { 160 | "class": "kd", 161 | "value": "func" 162 | }, 163 | { 164 | "value": " main()" 165 | } 166 | ] 167 | ], 168 | "language": "go" 169 | } 170 | ] 171 | }, 172 | { 173 | "start_line": 3, 174 | "start_char": 2, 175 | "definition_path": "main.go#L4", 176 | "hover": [ 177 | { 178 | "tokens": [ 179 | [ 180 | { 181 | "class": "kn", 182 | "value": "package" 183 | }, 184 | { 185 | "value": " " 186 | }, 187 | { 188 | "class": "s", 189 | "value": "\"github.com/user/hello/morestrings\"" 190 | } 191 | ] 192 | ], 193 | "language": "go" 194 | }, 195 | { 196 | "value": "Package morestrings implements additional functions to manipulate UTF-8 encoded strings, beyond what is provided in the standard \"strings\" package. \n\n" 197 | } 198 | ], 199 | "references": [ 200 | { 201 | "path": "main.go#L8" 202 | }, 203 | { 204 | "path": "main.go#L9" 205 | } 206 | ] 207 | } 208 | ] -------------------------------------------------------------------------------- /parser/lsif/ranges.go: -------------------------------------------------------------------------------- 1 | package lsif 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "strconv" 7 | 8 | "github.com/goccy/go-json" 9 | ) 10 | 11 | const ( 12 | definitions = "definitions" 13 | references = "references" 14 | ) 15 | 16 | type Ranges struct { 17 | DefRefs map[Id]Item 18 | References *References 19 | Hovers *Hovers 20 | Cache Cache 21 | 22 | NextMap map[Id]Id 23 | TextReferenceMap map[Id]Id 24 | TextDefinitionMap map[Id]Id 25 | RawEdgeMap map[Id][]RawItem 26 | } 27 | 28 | type Next struct { 29 | Id Id `json:"id"` 30 | Type string `json:"type"` 31 | Label string `json:"label"` 32 | OutV Id `json:"outV"` 33 | InV Id `json:"inV"` 34 | } 35 | 36 | type TextReference struct { 37 | Id Id `json:"id"` 38 | Type string `json:"type"` 39 | Label string `json:"label"` 40 | OutV Id `json:"outV"` 41 | InV Id `json:"inV"` 42 | } 43 | 44 | type RawRange struct { 45 | Id Id `json:"id"` 46 | Data Range `json:"start"` 47 | End Range `json:"end"` 48 | } 49 | 50 | type Range struct { 51 | Line int32 `json:"line"` 52 | Character int32 `json:"character"` 53 | Length int32 `json:"length"` 54 | RefId Id 55 | } 56 | 57 | type RawItem struct { 58 | Property string `json:"property"` 59 | RefId Id `json:"outV"` 60 | RangeIds []Id `json:"inVs"` 61 | DocId Id `json:"document"` 62 | } 63 | 64 | type Item struct { 65 | Line int32 66 | DocId Id 67 | RangeId Id 68 | } 69 | 70 | type SerializedRange struct { 71 | StartLine int32 `json:"start_line"` 72 | StartChar int32 `json:"start_char"` 73 | DefinitionPath string `json:"definition_path,omitempty"` 74 | Hover json.RawMessage `json:"hover"` 75 | References []SerializedReference `json:"references,omitempty"` 76 | } 77 | 78 | func NewRanges() (*Ranges, error) { 79 | hovers, err := NewHovers() 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | references, err := NewReferences() 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | cache, err := newCache("ranges", Range{}) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return &Ranges{ 95 | DefRefs: make(map[Id]Item), 96 | References: references, 97 | Hovers: hovers, 98 | Cache: cache, 99 | NextMap: make(map[Id]Id), 100 | TextReferenceMap: make(map[Id]Id), 101 | TextDefinitionMap: make(map[Id]Id), 102 | RawEdgeMap: map[Id][]RawItem{}, 103 | }, nil 104 | } 105 | 106 | func (r *Ranges) Read(label string, line []byte) error { 107 | switch label { 108 | case "range": 109 | if err := r.addRange(line); err != nil { 110 | return err 111 | } 112 | case "item": 113 | if err := r.addItem(line); err != nil { 114 | return err 115 | } 116 | default: 117 | switch label { 118 | case "next": 119 | var rawNext Next 120 | if err := json.Unmarshal(line, &rawNext); err != nil { 121 | return err 122 | } 123 | r.NextMap[rawNext.OutV] = rawNext.InV 124 | case "textDocument/references": 125 | var textReference TextReference 126 | if err := json.Unmarshal(line, &textReference); err != nil { 127 | return err 128 | } 129 | r.TextReferenceMap[textReference.OutV] = textReference.InV 130 | case "textDocument/definition": 131 | var textReference TextReference 132 | if err := json.Unmarshal(line, &textReference); err != nil { 133 | return err 134 | } 135 | r.TextDefinitionMap[textReference.OutV] = textReference.InV 136 | } 137 | 138 | // currently we do not need hover 139 | // return r.Hovers.Read(label, line) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (r *Ranges) Serialize(f io.Writer, rangeIds []Id, docs map[Id]string) error { 146 | encoder := json.NewEncoder(f) 147 | n := len(rangeIds) 148 | 149 | if _, err := f.Write([]byte("[")); err != nil { 150 | return err 151 | } 152 | 153 | for i, rangeId := range rangeIds { 154 | entry, err := r.getRange(rangeId) 155 | if err != nil { 156 | continue 157 | } 158 | 159 | serializedRange := SerializedRange{ 160 | StartLine: entry.Line, 161 | StartChar: entry.Character, 162 | DefinitionPath: r.definitionPathFor(docs, entry.RefId), 163 | Hover: r.Hovers.For(entry.RefId), 164 | References: r.References.For(docs, entry.RefId), 165 | } 166 | if err := encoder.Encode(serializedRange); err != nil { 167 | return err 168 | } 169 | if i+1 < n { 170 | if _, err := f.Write([]byte(",")); err != nil { 171 | return err 172 | } 173 | } 174 | } 175 | 176 | if _, err := f.Write([]byte("]")); err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func (r *Ranges) Close() error { 184 | for _, err := range []error{ 185 | r.Cache.Close(), 186 | r.References.Close(), 187 | r.Hovers.Close(), 188 | } { 189 | if err != nil { 190 | return err 191 | } 192 | } 193 | return nil 194 | } 195 | 196 | func (r *Ranges) definitionPathFor(docs map[Id]string, refId Id) string { 197 | defRef, ok := r.DefRefs[refId] 198 | if !ok { 199 | return "" 200 | } 201 | 202 | defPath := docs[defRef.DocId] + "#L" + strconv.Itoa(int(defRef.Line)) 203 | 204 | return defPath 205 | } 206 | 207 | func (r *Ranges) addRange(line []byte) error { 208 | var rg RawRange 209 | if err := json.Unmarshal(line, &rg); err != nil { 210 | return err 211 | } 212 | 213 | length := rg.End.Character - rg.Data.Character 214 | if length > 0 { 215 | rg.Data.Length = length 216 | } 217 | 218 | return r.Cache.SetEntry(rg.Id, &rg.Data) 219 | } 220 | 221 | func (r *Ranges) addItem(line []byte) error { 222 | var rawItem RawItem 223 | if err := json.Unmarshal(line, &rawItem); err != nil { 224 | return err 225 | } 226 | 227 | // store these edges whatever 228 | if l, ok := r.RawEdgeMap[rawItem.RefId]; ok { 229 | l = append(l, rawItem) 230 | } else { 231 | r.RawEdgeMap[rawItem.RefId] = []RawItem{rawItem} 232 | } 233 | 234 | if rawItem.Property != definitions && rawItem.Property != references { 235 | return nil 236 | } 237 | 238 | if len(rawItem.RangeIds) == 0 { 239 | return errors.New("no range IDs") 240 | } 241 | 242 | var references []Item 243 | 244 | for _, rangeId := range rawItem.RangeIds { 245 | rg, err := r.getRange(rangeId) 246 | if err != nil { 247 | return err 248 | } 249 | 250 | rg.RefId = rawItem.RefId 251 | 252 | if err := r.Cache.SetEntry(rangeId, rg); err != nil { 253 | return err 254 | } 255 | 256 | item := Item{ 257 | Line: rg.Line + 1, 258 | DocId: rawItem.DocId, 259 | RangeId: rangeId, 260 | } 261 | 262 | if rawItem.Property == definitions { 263 | r.DefRefs[rawItem.RefId] = item 264 | } else { 265 | references = append(references, item) 266 | } 267 | } 268 | 269 | if err := r.References.Store(rawItem.RefId, references); err != nil { 270 | return err 271 | } 272 | 273 | return nil 274 | } 275 | 276 | func (r *Ranges) getRange(rangeId Id) (*Range, error) { 277 | var rg Range 278 | if err := r.Cache.Entry(rangeId, &rg); err != nil { 279 | return nil, err 280 | } 281 | 282 | return &rg, nil 283 | } 284 | -------------------------------------------------------------------------------- /parser/lsif/testdata/expected/lsif/morestrings/reverse.go.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "start_line": 11, 4 | "start_char": 5, 5 | "definition_path": "morestrings/reverse.go#L12", 6 | "hover": [ 7 | { 8 | "tokens": [ 9 | [ 10 | { 11 | "class": "kd", 12 | "value": "func" 13 | }, 14 | { 15 | "value": " Reverse(s " 16 | }, 17 | { 18 | "class": "kt", 19 | "value": "string" 20 | }, 21 | { 22 | "value": ") " 23 | }, 24 | { 25 | "class": "kt", 26 | "value": "string" 27 | } 28 | ] 29 | ], 30 | "language": "go" 31 | }, 32 | { 33 | "value": "This method reverses a string \n\n" 34 | } 35 | ], 36 | "references": [ 37 | { 38 | "path": "main.go#L8" 39 | } 40 | ] 41 | }, 42 | { 43 | "start_line": 4, 44 | "start_char": 11, 45 | "definition_path": "morestrings/reverse.go#L5", 46 | "hover": [ 47 | { 48 | "tokens": [ 49 | [ 50 | { 51 | "class": "kd", 52 | "value": "var" 53 | }, 54 | { 55 | "value": " i " 56 | }, 57 | { 58 | "class": "kt", 59 | "value": "int" 60 | } 61 | ] 62 | ], 63 | "language": "go" 64 | } 65 | ] 66 | }, 67 | { 68 | "start_line": 11, 69 | "start_char": 13, 70 | "definition_path": "morestrings/reverse.go#L12", 71 | "hover": [ 72 | { 73 | "tokens": [ 74 | [ 75 | { 76 | "class": "kd", 77 | "value": "var" 78 | }, 79 | { 80 | "value": " s " 81 | }, 82 | { 83 | "class": "kt", 84 | "value": "string" 85 | } 86 | ] 87 | ], 88 | "language": "go" 89 | } 90 | ] 91 | }, 92 | { 93 | "start_line": 12, 94 | "start_char": 1, 95 | "definition_path": "morestrings/reverse.go#L13", 96 | "hover": [ 97 | { 98 | "tokens": [ 99 | [ 100 | { 101 | "class": "kd", 102 | "value": "var" 103 | }, 104 | { 105 | "value": " a " 106 | }, 107 | { 108 | "class": "kt", 109 | "value": "string" 110 | } 111 | ] 112 | ], 113 | "language": "go" 114 | } 115 | ], 116 | "references": [ 117 | { 118 | "path": "morestrings/reverse.go#L15" 119 | } 120 | ] 121 | }, 122 | { 123 | "start_line": 5, 124 | "start_char": 1, 125 | "definition_path": "morestrings/reverse.go#L6", 126 | "hover": [ 127 | { 128 | "tokens": [ 129 | [ 130 | { 131 | "class": "kd", 132 | "value": "var" 133 | }, 134 | { 135 | "value": " b " 136 | }, 137 | { 138 | "class": "kt", 139 | "value": "string" 140 | } 141 | ] 142 | ], 143 | "language": "go" 144 | } 145 | ], 146 | "references": [ 147 | { 148 | "path": "morestrings/reverse.go#L8" 149 | } 150 | ] 151 | }, 152 | { 153 | "start_line": 14, 154 | "start_char": 8, 155 | "definition_path": "morestrings/reverse.go#L13", 156 | "hover": [ 157 | { 158 | "tokens": [ 159 | [ 160 | { 161 | "class": "kd", 162 | "value": "var" 163 | }, 164 | { 165 | "value": " a " 166 | }, 167 | { 168 | "class": "kt", 169 | "value": "string" 170 | } 171 | ] 172 | ], 173 | "language": "go" 174 | } 175 | ], 176 | "references": [ 177 | { 178 | "path": "morestrings/reverse.go#L15" 179 | } 180 | ] 181 | }, 182 | { 183 | "start_line": 7, 184 | "start_char": 8, 185 | "definition_path": "morestrings/reverse.go#L6", 186 | "hover": [ 187 | { 188 | "tokens": [ 189 | [ 190 | { 191 | "class": "kd", 192 | "value": "var" 193 | }, 194 | { 195 | "value": " b " 196 | }, 197 | { 198 | "class": "kt", 199 | "value": "string" 200 | } 201 | ] 202 | ], 203 | "language": "go" 204 | } 205 | ], 206 | "references": [ 207 | { 208 | "path": "morestrings/reverse.go#L8" 209 | } 210 | ] 211 | }, 212 | { 213 | "start_line": 4, 214 | "start_char": 5, 215 | "definition_path": "morestrings/reverse.go#L5", 216 | "hover": [ 217 | { 218 | "tokens": [ 219 | [ 220 | { 221 | "class": "kd", 222 | "value": "func" 223 | }, 224 | { 225 | "value": " Func2(i " 226 | }, 227 | { 228 | "class": "kt", 229 | "value": "int" 230 | }, 231 | { 232 | "value": ") " 233 | }, 234 | { 235 | "class": "kt", 236 | "value": "string" 237 | } 238 | ] 239 | ], 240 | "language": "go" 241 | } 242 | ], 243 | "references": [ 244 | { 245 | "path": "main.go#L9" 246 | } 247 | ] 248 | } 249 | ] -------------------------------------------------------------------------------- /graph/visual/g6/template/report.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | srctx report 6 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | 86 | 87 | 88 | 89 | 93 | 94 | 95 |
CategoryCount
Total Impacts
76 | 77 | Direct Impacts 78 |
83 | 84 | In-Direct Impacts 85 |
90 | 91 | Potential Impacts 92 |
96 | 97 |
98 | 99 | 102 | 103 | 104 | 105 | 265 | 266 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!TIP] 2 | > Now we have another more powerful alternative in Rust, 3 | > which offering a more general solution for building relationships between source files, 4 | > without runtime indexing (like LSIF/SCIP). 5 | > 6 | > See: https://github.com/williamfzc/gossiphs 7 | 8 | --- 9 | 10 |

11 | 12 |

13 | 14 |

srctx: source context

15 |

16 | A library for extracting and analyzing definition/reference graphs from your codebase. Powered by tree-sitter and LSIF/SCIP. 17 |

18 | 19 | | Name | Status | 20 | |----------------|---------------------------------------------------------------------------------------------------------------------------------------------------| 21 | | Latest Version | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/williamfzc/srctx) | 22 | | Unit Tests | [![Go](https://github.com/williamfzc/srctx/actions/workflows/ci.yml/badge.svg)](https://github.com/williamfzc/srctx/actions/workflows/ci.yml) | 23 | | Code Coverage | [![codecov](https://codecov.io/github/williamfzc/srctx/branch/main/graph/badge.svg?token=1DuAXh12Ys)](https://codecov.io/github/williamfzc/srctx) | 24 | | Code Style | [![Go Report Card](https://goreportcard.com/badge/github.com/williamfzc/srctx)](https://goreportcard.com/report/github.com/williamfzc/srctx) | 25 | 26 | ## About this tool 27 | 28 | This library processes your code into precise function-level graphs, seamlessly integrated with Git, and then you can apply some analysis to them. 29 | 30 | image 31 | 32 | With this lib developers can know exactly what happened in every lines of your code. Such as definition, reference. And understand the actual impacts of your git commits. 33 | 34 | Some "dangerous" line changes can be found automatically. 35 | 36 | image 37 | 38 | You can see a dangerous change in file `cmd/srctx/diff/cmd.go#L29-#143`, . 39 | 40 | In addition, as a library, it also provides convenient APIs for secondary development, allowing you to freely access the content in the graph. 41 | 42 | ```golang 43 | src := filepath.Dir(filepath.Dir(curFile)) 44 | lsif := "../dump.lsif" 45 | lang := core.LangGo 46 | 47 | funcGraph, _ := function.CreateFuncGraphFromDirWithLSIF(src, lsif, lang) 48 | 49 | functions := funcGraph.GetFunctionsByFile("cmd/srctx/main.go") 50 | for _, each := range functions { 51 | // about this function 52 | log.Infof("func: %v", each.Id()) 53 | log.Infof("decl location: %v", each.FuncPos.Repr()) 54 | log.Infof("func name: %v", each.Name) 55 | 56 | // context of this function 57 | outVs := funcGraph.DirectReferencedIds(each) 58 | log.Infof("this function reached by %v other functions", len(outVs)) 59 | for _, eachOutV := range outVs { 60 | outV, _ := funcGraph.GetById(eachOutV) 61 | log.Infof("%v directly reached by %v", each.Name, outV.Name) 62 | } 63 | } 64 | ``` 65 | 66 | Output: 67 | 68 | ```text 69 | time="2023-06-17T19:48:52+08:00" level=info msg="func: cmd/srctx/main.go:#16-#26:main||mainFunc|[]string|" 70 | time="2023-06-17T19:48:52+08:00" level=info msg="decl location: cmd/srctx/main.go#16-26" 71 | time="2023-06-17T19:48:52+08:00" level=info msg="func name: mainFunc" 72 | time="2023-06-17T19:48:52+08:00" level=info msg="this function reached by 7 other functions" 73 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestDiffDir" 74 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestDiffNoDiff" 75 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestRenderHtml" 76 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestDiffRaw" 77 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestDiffSpecificLang" 78 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by TestDiff" 79 | time="2023-06-17T19:48:52+08:00" level=info msg="mainFunc directly reached by main" 80 | ``` 81 | 82 | > Currently, srctx is still in an active development phase. 83 | > If you're interested in its iteration direction and vision, you can check out [our roadmap page](https://github.com/williamfzc/srctx/issues/31). 84 | 85 | # Usage 86 | 87 | ## Quick Start 88 | 89 | We provide a one-click script for quickly deploying srctx anywhere. Common parameters include: 90 | 91 | - SRCTX_LANG: Required, specifies the language, such as GOLANG/JAVA/KOTLIN. 92 | - SRCTX_BUILD_CMD: Optional, specifies the compilation command. 93 | 94 | ### For Golang 95 | 96 | ```bash 97 | curl https://raw.githubusercontent.com/williamfzc/srctx/main/scripts/quickstart.sh \ 98 | | SRCTX_LANG=GOLANG bash 99 | ``` 100 | 101 | ### For Java/Kotlin 102 | 103 | As there is no unique compilation toolchain for Java (it could be Maven or Gradle, for example), 104 | so at the most time, you also need to specify the compilation command to obtain the invocation information. 105 | 106 | You should replace the `SRCTX_BUILD_CMD` with your own one. 107 | 108 | Java: 109 | 110 | ```bash 111 | curl https://raw.githubusercontent.com/williamfzc/srctx/main/scripts/quickstart.sh \ 112 | | SRCTX_LANG=JAVA SRCTX_BUILD_CMD="clean package -DskipTests" bash 113 | ``` 114 | 115 | Kotlin: 116 | 117 | Change the `SRCTX_LANG=JAVA` to `SRCTX_LANG=KOTLIN`. 118 | 119 | ## In Production 120 | 121 | In proudction, it is generally recommended to separate the indexing process from the analysis process, rather than using a one-click script to complete the entire process. This can make the entire process easier to maintain. 122 | 123 | ### 1. Generate LSIF file 124 | 125 | Tools can be found in https://lsif.dev/ . 126 | 127 | You will get a `dump.lsif` file after that. 128 | 129 | ### 2. Run `srctx` 130 | 131 | Download our prebuilt binaries from [release page](https://github.com/williamfzc/srctx/releases). 132 | 133 | For example, diff from `HEAD~1` to `HEAD`: 134 | 135 | ```bash 136 | ./srctx diff \ 137 | --before HEAD~1 \ 138 | --after HEAD \ 139 | --lsif dump.lsif \ 140 | --outputCsv output.csv \ 141 | --outputDot output.dot \ 142 | --outputHtml output.html 143 | ``` 144 | 145 | See details with `./srctx diff --help`. 146 | 147 | ### Prefer a real world sample? 148 | 149 | [Our CI](https://github.com/williamfzc/srctx/blob/main/.github/workflows/ci.yml) is a good start. 150 | 151 | ## Usage as Github Action (Recommendation) 152 | 153 | Because LSIF files require dev env heavily, it's really hard to provide a universal solution in a single binary file for 154 | all the repos. 155 | 156 | image 157 | 158 | We are currently working on [diffctx](https://github.com/williamfzc/diffctx), which will provide a GitHub Actions plugin 159 | that allows users to use it directly in a Pull Request. 160 | 161 | ## Usage as Lib 162 | 163 | ### API 164 | 165 | This API allows developers accessing the data of FuncGraph. 166 | 167 | - Start up example: [example/api_test.go](example/api_test.go) 168 | - Real world example: [cmd/srctx/diff/cmd.go](cmd/srctx/diff/cmd.go) 169 | 170 | ### Low level API 171 | 172 | Low level API allows developers consuming LSIF file directly. 173 | 174 | See [example/api_base_test.go](example/api_base_test.go) for details. 175 | 176 | # Correctness / Accuracy 177 | 178 | image 179 | 180 | We wanted it to provide detection capabilities as accurate as an IDE. 181 | 182 | # Roadmap 183 | 184 | See [Roadmap Issue](https://github.com/williamfzc/srctx/issues/31). 185 | 186 | # Contribution 187 | 188 | [Issues and PRs](https://github.com/williamfzc/srctx/issues) are always welcome. 189 | 190 | # References 191 | 192 | LSIF is a standard format for persisted code analyzer output. 193 | Today, several companies are working to support its growth, including Sourcegraph and GitHub/Microsoft. 194 | The LSIF defines a standard format for language servers or other programming tools to emit their knowledge about a code 195 | workspace. 196 | 197 | - https://lsif.dev/ 198 | - https://microsoft.github.io/language-server-protocol/overviews/lsif/overview/ 199 | - https://code.visualstudio.com/blogs/2019/02/19/lsif#_how-to-get-started 200 | 201 | # Thanks 202 | 203 | - SCIP/LSIF toolchains from https://github.com/sourcegraph 204 | - LSIF from Microsoft 205 | - LSIF parser from GitLab 206 | - IDE support from JetBrains 207 | 208 | # License 209 | 210 | [Apache 2.0](LICENSE) 211 | --------------------------------------------------------------------------------