├── .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: "
| Category | 68 |Count | 69 |
|---|---|
| Total Impacts | 72 |73 | |
| 76 | 77 | Direct Impacts 78 | | 79 |80 | |
| 83 | 84 | In-Direct Impacts 85 | | 86 |87 | |
| 90 | 91 | Potential Impacts 92 | | 93 |94 | |
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 |  | 22 | | Unit Tests | [](https://github.com/williamfzc/srctx/actions/workflows/ci.yml) | 23 | | Code Coverage | [](https://codecov.io/github/williamfzc/srctx) | 24 | | Code Style | [](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 |
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 |