Skip to content

Commit

Permalink
FEATURE: code analysis supports multiple classes per source file
Browse files Browse the repository at this point in the history
  • Loading branch information
christoph-daehne committed Oct 7, 2023
1 parent 89cfc5b commit 1fd0629
Show file tree
Hide file tree
Showing 15 changed files with 84 additions and 65 deletions.
63 changes: 34 additions & 29 deletions analysis/codeAnalyzer.go
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
package analysis

import (
"github.com/sandstorm/dependency-analysis/dataStructures"
"github.com/sandstorm/dependency-analysis/parsing"
"os"
"path/filepath"
"regexp"

"github.com/sandstorm/dependency-analysis/dataStructures"
"github.com/sandstorm/dependency-analysis/parsing"
)

// mapping from file path to source-unit
type sourceUnitByFile = map[string][]string
type sourceUnitByFileType = map[string][][]string

// mapping from source-unit to all its imports
type dependenciesBySourceUnit = map[string]*dataStructures.StringSet

func BuildDependencyGraph(settings *AnalyzerSettings) (*dataStructures.DirectedStringGraph, error) {
sourceUnits := make(sourceUnitByFile)
sourceUnitsByFile := make(sourceUnitByFileType)
if err := filepath.Walk(settings.SourcePath, initializeParsers(settings.IncludePattern)); err != nil {
return nil, err
}
if err := filepath.Walk(settings.SourcePath, findSourceUnits(settings.IncludePattern, sourceUnits)); err != nil {
if err := filepath.Walk(settings.SourcePath, findSourceUnits(settings.IncludePattern, sourceUnitsByFile)); err != nil {
return nil, err
}
var rootPackage []string = nil
for _, sourceUnit := range sourceUnits {
if rootPackage == nil {
rootPackage = sourceUnit
} else {
commonPrefixLength := getCommonPrefixLength(rootPackage, sourceUnit)
rootPackage = rootPackage[:commonPrefixLength]
for _, sourceUnits := range sourceUnitsByFile {
for _, sourceUnit := range sourceUnits {
if rootPackage == nil {
rootPackage = sourceUnit
} else {
commonPrefixLength := getCommonPrefixLength(rootPackage, sourceUnit)
rootPackage = rootPackage[:commonPrefixLength]
}
}
}

return findDependencies(rootPackage, sourceUnits, settings.Depth)
return findDependencies(rootPackage, sourceUnitsByFile, settings.Depth)
}

func initializeParsers(includePattern *regexp.Regexp) filepath.WalkFunc {
Expand All @@ -47,7 +50,7 @@ func initializeParsers(includePattern *regexp.Regexp) filepath.WalkFunc {
}
}

func findSourceUnits(includePattern *regexp.Regexp, result sourceUnitByFile) filepath.WalkFunc {
func findSourceUnits(includePattern *regexp.Regexp, result sourceUnitByFileType) filepath.WalkFunc {
return func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
Expand All @@ -58,9 +61,9 @@ func findSourceUnits(includePattern *regexp.Regexp, result sourceUnitByFile) fil
return err
}
defer fileReader.Close()
sourceUnit := parsing.ParseSourceUnit(path, fileReader)
if len(sourceUnit) > 0 {
result[path] = sourceUnit
sourceUnits := parsing.ParseSourceUnit(path, fileReader)
if len(sourceUnits) > 0 {
result[path] = sourceUnits
}
}
return nil
Expand All @@ -77,11 +80,11 @@ func getCommonPrefixLength(left []string, right []string) int {
return limit
}

func findDependencies(rootPackage []string, sourceUnits sourceUnitByFile, depth int) (*dataStructures.DirectedStringGraph, error) {
func findDependencies(rootPackage []string, sourceUnitsByFile sourceUnitByFileType, depth int) (*dataStructures.DirectedStringGraph, error) {
dependencyGraph := dataStructures.NewDirectedStringGraph()
prefixLength := len(rootPackage)
segmentLimit := len(rootPackage) + depth
for path, sourceUnit := range sourceUnits {
for path, sourceUnits := range sourceUnitsByFile {
fileReader, err := os.Open(path)
if err != nil {
return nil, err
Expand All @@ -91,17 +94,19 @@ func findDependencies(rootPackage []string, sourceUnits sourceUnitByFile, depth
if err != nil {
return nil, err
}
sourceUnitString := parsing.JoinPathSegments(
path,
sourceUnit[prefixLength:min(segmentLimit, len(sourceUnit))])
dependencyGraph.AddNode(sourceUnitString)
for _, dependency := range allDependencies {
if arrayStartsWith(dependency, rootPackage) {
dependencyString := parsing.JoinPathSegments(
path,
dependency[prefixLength:min(segmentLimit, len(dependency))])
if sourceUnitString != dependencyString {
dependencyGraph.AddEdge(sourceUnitString, dependencyString)
for _, sourceUnit := range sourceUnits {
sourceUnitString := parsing.JoinPathSegments(
path,
sourceUnit[prefixLength:min(segmentLimit, len(sourceUnit))])
dependencyGraph.AddNode(sourceUnitString)
for _, dependency := range allDependencies {
if arrayStartsWith(dependency, rootPackage) {
dependencyString := parsing.JoinPathSegments(
path,
dependency[prefixLength:min(segmentLimit, len(dependency))])
if sourceUnitString != dependencyString {
dependencyGraph.AddEdge(sourceUnitString, dependencyString)
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion analysis/codeAnalyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestBuildDependencyGraph(t *testing.T) {
AddEdge("analysis", "parsing").
AddEdge("analysis", "dataStructures").
AddEdge("parsing", "dataStructures").
AddEdge("rendering", "dataStructuress")
AddEdge("rendering", "dataStructures")
AssertEquals(t, "incorrect graph", expected, actual)
})
}
14 changes: 11 additions & 3 deletions dataStructures/directedGraph.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package dataStructures

import "fmt"
import (
"fmt"
"sort"
)

// directed graph with nodes of type string
type DirectedStringGraph struct {
Expand Down Expand Up @@ -55,8 +58,13 @@ func (this *DirectedStringGraph) GetEdges() map[string][]string {

func (this *DirectedStringGraph) String() string {
result := "{ "
for source, targets := range this.Edges {
result += fmt.Sprintf("%v -> %v ", source, targets)
sources := make([]string, 0, len(this.Edges))
for source := range this.Edges {
sources = append(sources, source)
}
sort.Strings(sources)
for _, source := range sources {
result += fmt.Sprintf("%v -> %v ", source, this.Edges[source])
}
return result + "}"
}
9 changes: 7 additions & 2 deletions dataStructures/stringSet.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package dataStructures

import (
"fmt"
"sort"
)

type StringSet struct {
Expand Down Expand Up @@ -44,8 +44,13 @@ func (this *StringSet) ToArray() []string {

func (this *StringSet) String() string {
result := "[ "
keys := make([]string, 0, len(this.content))
for key := range this.content {
result += fmt.Sprintf("%v ", key)
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
result += key + " "
}
return result + "]"
}
4 changes: 2 additions & 2 deletions parsing/codeParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func InitializeParsers(filePath string) error {
// The package path is already split by the language's delimiter,
// e.g. in Java de.sandstorm.test.helpers.ListHelpers results in
// [de sandstorm test helpers ListHelpers]
func ParseSourceUnit(sourcePath string, fileReader io.Reader) []string {
func ParseSourceUnit(sourcePath string, fileReader io.Reader) [][]string {
switch {
case strings.HasSuffix(sourcePath, ".go"):
filePathSplit := strings.Split(sourcePath, "/")
Expand All @@ -47,7 +47,7 @@ func ParseSourceUnit(sourcePath string, fileReader io.Reader) []string {
case strings.HasSuffix(sourcePath, ".jsx"):
return ParseJavaScriptSourceUnit(sourcePath)
}
return []string{}
return [][]string{}
}

// 3rd step during code analysis, called for each source unit (see 2nc step)
Expand Down
6 changes: 3 additions & 3 deletions parsing/goParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ func ParseGoMod(filePath string) error {
return nil
}

func ParseGoSourceUnit(fileName string, fileReader io.Reader) []string {
func ParseGoSourceUnit(fileName string, fileReader io.Reader) [][]string {
packageString := getFirstLineMatchInReader(fileReader, golangParser.packageRegex)
if packageString != "" {
packagePath := strings.Split(packageString, "/")
return append(golangParser.modulePath, append(packagePath, fileName)...)
return [][]string{append(golangParser.modulePath, append(packagePath, fileName)...)}
} else {
return []string{}
return [][]string{}
}
}

Expand Down
2 changes: 1 addition & 1 deletion parsing/goParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestParseGoSourceUnit(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
file := bytes.NewBufferString(testCase.fileContent)
AssertEquals(t,
testCase.expected,
[][]string{testCase.expected},
ParseGoSourceUnit(testCase.fileName, file),
)
})
Expand Down
8 changes: 4 additions & 4 deletions parsing/groovyParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ var groovyParser = struct {
importRegex: regexp.MustCompile(`import\s+(?:static\s+)?([^; \n]+)\s*;?`),
}

func ParseGroovySourceUnit(fileReader io.Reader) []string {
func ParseGroovySourceUnit(fileReader io.Reader) [][]string {
scanner := bufio.NewScanner(fileReader)
scanner.Split(bufio.ScanLines)
packageString := getFirstLineMatchInScanner(scanner, groovyParser.packageRegex)
className := getFirstLineMatchInScanner(scanner, groovyParser.classRegex)
if packageString != "" && className != "" {
return append(strings.Split(packageString, "."), className)
return [][]string{append(strings.Split(packageString, "."), className)}
}
if className != "" {
return []string{className}
return [][]string{[]string{className}}
}
return []string{}
return [][]string{}
}

func ParseGroovyImports(fileReader io.Reader) ([][]string, error) {
Expand Down
2 changes: 1 addition & 1 deletion parsing/groovyParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestParseGroovySourceUnit(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
file := bytes.NewBufferString(testCase.fileContent)
AssertEquals(t,
testCase.expected,
[][]string{testCase.expected},
ParseGroovySourceUnit(file),
)
})
Expand Down
8 changes: 4 additions & 4 deletions parsing/javaParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ var javaParser = struct {
importRegex: regexp.MustCompile(`import\s+(?:static\s+)?([^; ]+)\s*;`),
}

func ParseJavaSourceUnit(fileReader io.Reader) []string {
func ParseJavaSourceUnit(fileReader io.Reader) [][]string {
scanner := bufio.NewScanner(fileReader)
scanner.Split(bufio.ScanLines)
packageString := getFirstLineMatchInScanner(scanner, javaParser.packageRegex)
className := getFirstLineMatchInScanner(scanner, javaParser.classRegex)
if packageString != "" && className != "" {
return append(strings.Split(packageString, "."), className)
return [][]string{append(strings.Split(packageString, "."), className)}
}
if className != "" {
return []string{className}
return [][]string{[]string{className}}
}
return []string{}
return [][]string{}
}

func ParseJavaImports(fileReader io.Reader) ([][]string, error) {
Expand Down
3 changes: 2 additions & 1 deletion parsing/javaParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ func TestParseJavaSourceUnit(t *testing.T) {
expected: []string{"de", "sandstorm", "test", "Main"},
},
// TODO: test private static final class
// TODO: interfaces and enums (also other languages)
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
file := bytes.NewBufferString(testCase.fileContent)
AssertEquals(t,
testCase.expected,
[][]string{testCase.expected},
ParseJavaSourceUnit(file),
)
})
Expand Down
8 changes: 4 additions & 4 deletions parsing/javaScriptParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@ var javaScriptParser = struct {
importRegex: regexp.MustCompile(`(?:import\s+.*)?from\s+["']([^'"]+)["'];?`),
}

func ParseJavaScriptSourceUnit(sourcePath string) []string {
func ParseJavaScriptSourceUnit(sourcePath string) [][]string {
if strings.Contains(sourcePath, "node_modules") {
return []string{}
return [][]string{}
} else {
parent := filepath.Dir(sourcePath)
parentSegments := strings.Split(parent, "/")
fileName := filepath.Base(sourcePath)
fileBasename := strings.TrimSuffix(fileName, filepath.Ext(fileName))
if fileBasename == "index" {
return parentSegments
return [][]string{parentSegments}
} else {
return append(parentSegments, fileBasename)
return [][]string{append(parentSegments, fileBasename)}
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions parsing/javaScriptParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@ func TestParseJavaScriptSourceUnit(t *testing.T) {
testCases := []struct {
name string
sourcePath string
expected []string
expected [][]string
}{
{
name: "file path without dots",
sourcePath: "src/Components/Button/button.js",
expected: []string{"src", "Components", "Button", "button"},
expected: [][]string{[]string{"src", "Components", "Button", "button"}},
},
{
name: "file path with dots",
sourcePath: "a/.././src/a/../b/c/../.././Components/Button/button.js",
expected: []string{"src", "Components", "Button", "button"},
expected: [][]string{[]string{"src", "Components", "Button", "button"}},
},
{
name: "file path with index.js",
sourcePath: "a/.././src/a/../b/c/../.././Components/Button/index.js",
expected: []string{"src", "Components", "Button"},
expected: [][]string{[]string{"src", "Components", "Button"}},
},
{
name: "ignore node_modules",
sourcePath: "node_modules/some/lib.js",
expected: []string{},
expected: [][]string{},
},
}
for _, testCase := range testCases {
Expand Down
8 changes: 4 additions & 4 deletions parsing/phpParser.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ var phpParser = struct {
useRegex: regexp.MustCompile(`use\s+([^; ]+)\s*;`),
}

func ParsePhpSourceUnit(fileReader io.Reader) []string {
func ParsePhpSourceUnit(fileReader io.Reader) [][]string {
scanner := bufio.NewScanner(fileReader)
scanner.Split(bufio.ScanLines)
namespace := getFirstLineMatchInScanner(scanner, phpParser.namespaceRegex)
className := getFirstLineMatchInScanner(scanner, phpParser.classRegex)
if namespace != "" && className != "" {
return append(strings.Split(namespace, "\\"), className)
return [][]string{append(strings.Split(namespace, "\\"), className)}
}
if className != "" {
return []string{className}
return [][]string{[]string{className}}
}
return []string{}
return [][]string{}
}

func ParsePhpImports(fileReader io.Reader) ([][]string, error) {
Expand Down
2 changes: 1 addition & 1 deletion parsing/phpParser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestParsePhpSourceUnit(t *testing.T) {
t.Run(testCase.name, func(t *testing.T) {
file := bytes.NewBufferString(testCase.fileContent)
AssertEquals(t,
testCase.expected,
[][]string{testCase.expected},
ParsePhpSourceUnit(file),
)
})
Expand Down

0 comments on commit 1fd0629

Please sign in to comment.