Skip to content

Commit

Permalink
feat: Support Golang (#93)
Browse files Browse the repository at this point in the history
* Support Golang

* Lint

* Refactor imports

* Introduce extra forces for packages

* Add some tests

* Allow imports from multiple packages and tweak linter

* Fix test

* Improve go imports correctness

* Load LOC for go files

* Fix message

* Improve package aliasing correctness

* Add file object testing
  • Loading branch information
gabotechs authored Jun 8, 2024
1 parent 261078c commit 3e7f148
Show file tree
Hide file tree
Showing 24 changed files with 849 additions and 38 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- uses: actions/setup-go@v4
with:
go-version: '1.21'
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2
- run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.0
- run: golangci-lint run -v

tag:
Expand Down
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ linters-settings:
gocyclo:
min-complexity: 16
godot:
check-all: true
scope: all
goimports:
local-prefixes: dep-tree
gocritic:
Expand Down
2 changes: 1 addition & 1 deletion cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func ConfigCmd() *cobra.Command {
configPath = config.DefaultConfigPath
}
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
return os.WriteFile(configPath, []byte(config.SampleConfig), os.ModePerm)
return os.WriteFile(configPath, []byte(config.SampleConfig), 0o600)
} else {
return errors.New("Cannot generate config file, as one already exists in " + configPath)
}
Expand Down
16 changes: 15 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/gabotechs/dep-tree/internal/config"
"github.com/gabotechs/dep-tree/internal/dummy"
golang "github.com/gabotechs/dep-tree/internal/go"
"github.com/gabotechs/dep-tree/internal/js"
"github.com/gabotechs/dep-tree/internal/language"
"github.com/gabotechs/dep-tree/internal/python"
Expand Down Expand Up @@ -93,11 +94,16 @@ $ dep-tree check`,
return root
}

//nolint:gocyclo
func inferLang(files []string, cfg *config.Config) (language.Language, error) {
if len(files) == 0 {
return nil, fmt.Errorf("at least 1 file must be provided for infering the language")
}
score := struct {
js int
python int
rust int
golang int
dummy int
}{}
top := struct {
Expand All @@ -124,6 +130,12 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
top.v = score.python
top.lang = "python"
}
case utils.EndsWith(file, golang.Extensions):
score.golang += 1
if score.golang > top.v {
top.v = score.golang
top.lang = "golang"
}
case utils.EndsWith(file, dummy.Extensions):
score.dummy += 1
if score.dummy > top.v {
Expand All @@ -133,7 +145,7 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
}
}
if top.lang == "" {
return nil, errors.New("at least one file must be provided")
return nil, errors.New("none of the provided files belong to the a supported language")
}
switch top.lang {
case "js":
Expand All @@ -142,6 +154,8 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
return rust.MakeRustLanguage(&cfg.Rust)
case "python":
return python.MakePythonLanguage(&cfg.Python)
case "golang":
return golang.NewLanguage(files[0], &cfg.Golang)
case "dummy":
return &dummy.Language{}, nil
default:
Expand Down
7 changes: 6 additions & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ func TestInferLang(t *testing.T) {
Expected language.Language
Error string
}{
{
Name: "zero files",
Files: []string{},
Error: "at least 1 file must be provided for infering the language",
},
{
Name: "only 1 file",
Files: []string{"foo.js"},
Expand All @@ -133,7 +138,7 @@ func TestInferLang(t *testing.T) {
{
Name: "no match",
Files: []string{"foo.pdf", "bar.docx"},
Error: "at least one file must be provided",
Error: "none of the provided files belong to the a supported language",
},
}

Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"gopkg.in/yaml.v3"

"github.com/gabotechs/dep-tree/internal/check"
golang "github.com/gabotechs/dep-tree/internal/go"
"github.com/gabotechs/dep-tree/internal/js"
"github.com/gabotechs/dep-tree/internal/python"
"github.com/gabotechs/dep-tree/internal/rust"
Expand All @@ -28,6 +29,7 @@ type Config struct {
Js js.Config `yaml:"js"`
Rust rust.Config `yaml:"rust"`
Python python.Config `yaml:"python"`
Golang golang.Config `yaml:"golang"`
}

func (c *Config) UnwrapProxyExports() bool {
Expand Down
28 changes: 24 additions & 4 deletions internal/entropy/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ type Node struct {
Size int `json:"size"`
Color []int `json:"color,omitempty"`
IsDir bool `json:"isDir"`
IsPackage bool `json:"isPackage"`
}

type Link struct {
From int64 `json:"from"`
To int64 `json:"to"`
IsDir bool `json:"isDir"`
IsCyclic bool `json:"isCyclic"`
From int64 `json:"from"`
To int64 `json:"to"`
IsDir bool `json:"isDir"`
IsPackage bool `json:"isPackage"`
IsCyclic bool `json:"isCyclic"`
}

type Graph struct {
Expand All @@ -47,6 +49,7 @@ func toInt(arr []float64) []int {
return result
}

// TODO: factor this out
func makeGraph(files []string, parser graph.NodeParser[*language.FileInfo], loadCallbacks graph.LoadCallbacks[*language.FileInfo]) (Graph, error) {
g := graph.NewGraph[*language.FileInfo]()
err := g.Load(files, parser, loadCallbacks)
Expand Down Expand Up @@ -84,6 +87,7 @@ func makeGraph(files []string, parser graph.NodeParser[*language.FileInfo], load
}

addedFolders := map[string]bool{}
addedPackages := map[string]bool{}

for _, node := range allNodes {
dirName := filepath.Dir(node.Data.RelPath)
Expand Down Expand Up @@ -123,6 +127,22 @@ func makeGraph(files []string, parser graph.NodeParser[*language.FileInfo], load
})
}
}

if node.Data.Package != "" {
packageNode := graph.MakeNode(node.Data.Package, 0)
if _, ok := addedPackages[node.Data.Package]; !ok {
addedPackages[node.Data.Package] = true
out.Nodes = append(out.Nodes, Node{
Id: packageNode.ID(),
IsPackage: true,
})
}
out.Links = append(out.Links, Link{
From: node.ID(),
To: packageNode.ID(),
IsPackage: true,
})
}
}

for _, cycle := range cycles {
Expand Down
19 changes: 14 additions & 5 deletions internal/entropy/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<script type="importmap">{ "imports": { "three": "https://unpkg.com/three/build/three.module.js" }}</script>
<script type="module">
import {UnrealBloomPass} from 'https://unpkg.com/three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { UnrealBloomPass } from 'https://unpkg.com/three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { CSS2DRenderer, CSS2DObject } from 'https://unpkg.com/three/examples/jsm/renderers/CSS2DRenderer.js';

const DATA = {}
Expand All @@ -40,8 +40,10 @@
LINK_DISTANCE: 30, // https://github.com/vasturiano/d3-force-3d?tab=readme-ov-file#link_distance
FILE_NODE_REPULSION_FORCE: 30, // https://github.com/vasturiano/d3-force-3d?tab=readme-ov-file#manyBody_strength
DIR_NODE_REPULSION_FORCE: 40,
PACKAGE_NODE_REPULSION_FORCE: 40,
FILE_LINK_STRENGTH_FACTOR: 1,
DIR_LINK_STRENGTH_FACTOR: 2.5,
PACKAGE_LINK_STRENGTH_FACTOR: 5,
HIGHLIGHT_CYCLES: false
}

Expand Down Expand Up @@ -158,7 +160,7 @@
.nodeThreeObject(nodeThreeObject)
.nodeThreeObjectExtend(true)
.nodeVal('size')
.nodeVisibility(node => !node['isDir'])
.nodeVisibility(node => !node['isDir'] && !node['isPackage'])
.nodeColor(colorNode)
.onNodeClick(node => {
selectNode(node)
Expand All @@ -171,7 +173,7 @@
.linkDirectionalArrowColor(colorLink)
.linkSource('from')
.linkTarget('to')
.linkVisibility(link => !link['isDir'])
.linkVisibility(link => !link['isDir'] && !link['isPackage'])
.linkWidth(link => highlightLinks.has(link) ? SETTINGS.LINK_HIGHLIGHT_WIDTH : undefined)
.linkDirectionalParticles(link => highlightLinks.has(link) ? 2 : 0)
.linkDirectionalParticleWidth(SETTINGS.LINK_HIGHLIGHT_WIDTH);
Expand All @@ -182,13 +184,20 @@
.d3Force('link')
.distance(_link => SETTINGS.LINK_DISTANCE)
.strength(link => {
const f = link['isDir'] ? SETTINGS.DIR_LINK_STRENGTH_FACTOR : SETTINGS.FILE_LINK_STRENGTH_FACTOR
let f = SETTINGS.FILE_LINK_STRENGTH_FACTOR
if (link['isDir']) f = SETTINGS.DIR_LINK_STRENGTH_FACTOR
if (link['isPackage']) f = SETTINGS.PACKAGE_LINK_STRENGTH_FACTOR
return f / Math.min(NODES[link.from].neighbors.length, NODES[link.to].neighbors.length);
})

Graph
.d3Force('charge')
.strength(node => -(node['isDir'] ? SETTINGS.DIR_NODE_REPULSION_FORCE : SETTINGS.FILE_NODE_REPULSION_FORCE))
.strength(node => {
let f = SETTINGS.FILE_NODE_REPULSION_FORCE
if (node['isDir']) f = SETTINGS.DIR_NODE_REPULSION_FORCE
if (node['isPackage']) f = SETTINGS.PACKAGE_NODE_REPULSION_FORCE
return -f
})

let recomputeTimeout
function debouncedRecompute() {
Expand Down
2 changes: 1 addition & 1 deletion internal/entropy/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Render(files []string, parser graph.NodeParser[*language.FileInfo], cfg Ren
} else {
temp = filepath.Join(os.TempDir(), "index.html")
}
err = os.WriteFile(temp, rendered, os.ModePerm)
err = os.WriteFile(temp, rendered, 0o600)
if err != nil {
return err
}
Expand Down
3 changes: 3 additions & 0 deletions internal/go/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package golang

type Config struct{}
26 changes: 26 additions & 0 deletions internal/go/exports.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package golang

import (
"strings"

"github.com/gabotechs/dep-tree/internal/language"
)

func (l *Language) ParseExports(file *language.FileInfo) (*language.ExportsResult, error) {
content := file.Content.(*File)
results := language.ExportsResult{}
for symbol := range content.AstFile.Scope.Objects {
if len(symbol) == 0 {
continue
}
if symbol[:1] == strings.ToUpper(symbol[:1]) {
results.Exports = append(results.Exports, language.ExportEntry{
Symbols: []language.ExportSymbol{{
Original: symbol,
}},
AbsPath: file.AbsPath,
})
}
}
return &results, nil
}
52 changes: 52 additions & 0 deletions internal/go/exports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package golang

import (
"sort"
"testing"

"github.com/stretchr/testify/require"
)

func TestExports(t *testing.T) {
tests := []struct {
Name string
Expected []string
}{
{
Name: "exports.go",
Expected: []string{},
},
{
Name: "config.go",
Expected: []string{"Config"},
},
{
Name: "language.go",
Expected: []string{"Extensions", "Language", "NewLanguage"},
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
a := require.New(t)

lang, err := NewLanguage(".", &Config{})
a.NoError(err)
file, err := lang.ParseFile(tt.Name)
a.NoError(err)
exports, err := lang.ParseExports(file)
a.NoError(err)

actualExports := make([]string, 0)
for _, export := range exports.Exports {
for _, symbol := range export.Symbols {
actualExports = append(actualExports, symbol.Original)
}
}
sort.Strings(tt.Expected)
sort.Strings(actualExports)

a.Equal(tt.Expected, actualExports)
})
}
}
28 changes: 28 additions & 0 deletions internal/go/go_mod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package golang

import (
"os"

"github.com/gabotechs/dep-tree/internal/utils"
"golang.org/x/mod/modfile"
)

type GoMod struct {
Module string
}

func _ParseGoMod(file string) (*GoMod, error) {
modBytes, err := os.ReadFile(file)
if err != nil {
return nil, err
}
goMod, err := modfile.Parse(file, modBytes, nil)
if err != nil {
return nil, err
}
return &GoMod{
Module: goMod.Module.Mod.Path,
}, nil
}

var ParseGoMod = utils.Cached1In1OutErr(_ParseGoMod)
30 changes: 30 additions & 0 deletions internal/go/go_mod_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package golang

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestGoMod(t *testing.T) {
tests := []struct {
Name string
Expected GoMod
}{
{
Name: "../../go.mod",
Expected: GoMod{
Module: "github.com/gabotechs/dep-tree",
},
},
}

for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
a := require.New(t)
result, err := ParseGoMod(tt.Name)
a.NoError(err)
a.Equal(tt.Expected, *result)
})
}
}
Loading

0 comments on commit 3e7f148

Please sign in to comment.