Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/bmatcuk/doublestar/v4"
"github.com/gabotechs/dep-tree/internal/config"
"github.com/gabotechs/dep-tree/internal/cpp"
"github.com/gabotechs/dep-tree/internal/dummy"
golang "github.com/gabotechs/dep-tree/internal/go"
"github.com/gabotechs/dep-tree/internal/graph"
Expand Down Expand Up @@ -141,6 +142,7 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
python int
rust int
golang int
cpp int
dummy int
}{}
top := struct {
Expand Down Expand Up @@ -173,6 +175,12 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
top.v = score.golang
top.lang = "golang"
}
case utils.EndsWith(file, cpp.Extensions):
score.cpp += 1
if score.cpp > top.v {
top.v = score.cpp
top.lang = "cpp"
}
case utils.EndsWith(file, dummy.Extensions):
score.dummy += 1
if score.dummy > top.v {
Expand All @@ -193,6 +201,8 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
return python.MakePythonLanguage(&cfg.Python)
case "golang":
return golang.NewLanguage(files[0], &cfg.Golang)
case "cpp":
return cpp.NewLanguage(&cfg.Cpp), nil
case "dummy":
return &dummy.Language{}, nil
default:
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"gopkg.in/yaml.v3"

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

func NewConfigCwd() Config {
Expand Down
20 changes: 20 additions & 0 deletions internal/cpp/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package cpp

type Config struct {
IncludeDirs []string `yaml:"include_dirs"`

ExcludeSystemHeaders bool `yaml:"exclude_system_headers"`

HeaderExtensions []string `yaml:"header_extensions"`

SourceExtensions []string `yaml:"source_extensions"`
}

func DefaultConfig() *Config {
return &Config{
IncludeDirs: []string{},
ExcludeSystemHeaders: true,
HeaderExtensions: []string{".h", ".hpp", ".hh", ".hxx", ".h++"},
SourceExtensions: []string{".cpp", ".cc", ".cxx", ".c++"},
}
}
220 changes: 220 additions & 0 deletions internal/cpp/language.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
package cpp

import (
"bytes"
"os"
"path/filepath"
"strings"

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

var Extensions = []string{
"cpp", "cc", "cxx", "c++",
"hpp", "hh", "hxx", "h++", "h",
}

type Language struct {
Cfg *Config
}

func NewLanguage(cfg *Config) *Language {
if cfg == nil {
cfg = &Config{}
}
return &Language{Cfg: cfg}
}

func (l *Language) ParseFile(path string) (*language.FileInfo, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}

file, err := ParseCppFile(string(content))
if err != nil {
return nil, err
}

currentDir, _ := os.Getwd()
relPath, _ := filepath.Rel(currentDir, path)

return &language.FileInfo{
Content: file,
Loc: bytes.Count(content, []byte("\n")),
Size: len(content),
AbsPath: path,
RelPath: relPath,
}, nil
}

func (l *Language) ParseImports(file *language.FileInfo) (*language.ImportsResult, error) {
var result language.ImportsResult

cppFile, ok := file.Content.(*File)
if !ok {
return &result, nil
}

for _, include := range cppFile.Includes {
if include.IsSystem {
continue
}

absPath := l.resolveIncludePath(file.AbsPath, include.Header)
if absPath != "" {
result.Imports = append(result.Imports, language.ImportEntry{
All: true,
AbsPath: absPath,
})
}
}

return &result, nil
}

func (l *Language) ParseExports(file *language.FileInfo) (*language.ExportsResult, error) {
var result language.ExportsResult

// For C++, determining exports is complex as it depends on:
// - Public class/struct members
// - Free functions
// - Global variables
// - Template definitions
// For now, we'll treat header files as exporting everything
// and source files as exporting nothing by default

if l.isHeaderFile(file.AbsPath) {
// Header files export all their content
result.Exports = append(result.Exports, language.ExportEntry{
All: true,
AbsPath: file.AbsPath,
})
}

return &result, nil
}

func (l *Language) resolveIncludePath(sourceFile, includePath string) string {
// If the include path is absolute, return it as-is
if filepath.IsAbs(includePath) {
return includePath
}

// try relative to the source file directory
sourceDir := filepath.Dir(sourceFile)
resolvedPath := filepath.Join(sourceDir, includePath)

if _, err := os.Stat(resolvedPath); err == nil {
abs, _ := filepath.Abs(resolvedPath)
return abs
}

if !hasExtension(includePath) {
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
testPath := resolvedPath + ext
if _, err := os.Stat(testPath); err == nil {
abs, _ := filepath.Abs(testPath)
return abs
}
}
}

// try relative to project root
projectRoots := l.findProjectRoots(sourceDir)
for _, root := range projectRoots {
testPath := filepath.Join(root, includePath)
if _, err := os.Stat(testPath); err == nil {
abs, _ := filepath.Abs(testPath)
return abs
}

// Try with extensions
if !hasExtension(includePath) {
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
testPathWithExt := testPath + ext
if _, err := os.Stat(testPathWithExt); err == nil {
abs, _ := filepath.Abs(testPathWithExt)
return abs
}
}
}
}

// try supporting configured include directories
for _, includeDir := range l.Cfg.IncludeDirs {
var testPath string
if filepath.IsAbs(includeDir) {
testPath = filepath.Join(includeDir, includePath)
} else {
testPath = filepath.Join(sourceDir, includeDir, includePath)
}

if _, err := os.Stat(testPath); err == nil {
abs, _ := filepath.Abs(testPath)
return abs
}

// Try with extensions
if !hasExtension(includePath) {
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
testPathWithExt := testPath + ext
if _, err := os.Stat(testPathWithExt); err == nil {
abs, _ := filepath.Abs(testPathWithExt)
return abs
}
}
}
}

return ""
}

func (l *Language) findProjectRoots(startDir string) []string {
var roots []string
currentDir := startDir

// Common project root indicators
rootIndicators := []string{
"CMakeLists.txt", "Makefile", "SConstruct", // Build files
".git", ".hg", ".svn", // Version control
"package.json", "Cargo.toml", "go.mod", // Language-specific
"README.md", "README.txt", // Documentation
}

for {
for _, indicator := range rootIndicators {
if _, err := os.Stat(filepath.Join(currentDir, indicator)); err == nil {
roots = append(roots, currentDir)
break
}
}

parentDir := filepath.Dir(currentDir)
if parentDir == currentDir {
break
}
currentDir = parentDir

if len(roots) >= 3 {
break
}
}

return roots
}

func (l *Language) isHeaderFile(path string) bool {
ext := strings.ToLower(filepath.Ext(path))
headerExts := []string{".h", ".hpp", ".hh", ".hxx", ".h++"}
for _, headerExt := range headerExts {
if ext == headerExt {
return true
}
}
return false
}

func hasExtension(path string) bool {
return filepath.Ext(path) != ""
}
Loading