Skip to content

Commit 394fbee

Browse files
feat: add support for C++ language
1 parent 60a95a2 commit 394fbee

File tree

7 files changed

+574
-0
lines changed

7 files changed

+574
-0
lines changed

cmd/root.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/bmatcuk/doublestar/v4"
1010
"github.com/gabotechs/dep-tree/internal/config"
11+
"github.com/gabotechs/dep-tree/internal/cpp"
1112
"github.com/gabotechs/dep-tree/internal/dummy"
1213
golang "github.com/gabotechs/dep-tree/internal/go"
1314
"github.com/gabotechs/dep-tree/internal/graph"
@@ -141,6 +142,7 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
141142
python int
142143
rust int
143144
golang int
145+
cpp int
144146
dummy int
145147
}{}
146148
top := struct {
@@ -173,6 +175,12 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
173175
top.v = score.golang
174176
top.lang = "golang"
175177
}
178+
case utils.EndsWith(file, cpp.Extensions):
179+
score.cpp += 1
180+
if score.cpp > top.v {
181+
top.v = score.cpp
182+
top.lang = "cpp"
183+
}
176184
case utils.EndsWith(file, dummy.Extensions):
177185
score.dummy += 1
178186
if score.dummy > top.v {
@@ -193,6 +201,8 @@ func inferLang(files []string, cfg *config.Config) (language.Language, error) {
193201
return python.MakePythonLanguage(&cfg.Python)
194202
case "golang":
195203
return golang.NewLanguage(files[0], &cfg.Golang)
204+
case "cpp":
205+
return cpp.NewLanguage(&cfg.Cpp), nil
196206
case "dummy":
197207
return &dummy.Language{}, nil
198208
default:

internal/config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"gopkg.in/yaml.v3"
1212

1313
"github.com/gabotechs/dep-tree/internal/check"
14+
"github.com/gabotechs/dep-tree/internal/cpp"
1415
golang "github.com/gabotechs/dep-tree/internal/go"
1516
"github.com/gabotechs/dep-tree/internal/js"
1617
"github.com/gabotechs/dep-tree/internal/python"
@@ -33,6 +34,7 @@ type Config struct {
3334
Rust rust.Config `yaml:"rust"`
3435
Python python.Config `yaml:"python"`
3536
Golang golang.Config `yaml:"golang"`
37+
Cpp cpp.Config `yaml:"cpp"`
3638
}
3739

3840
func NewConfigCwd() Config {

internal/cpp/config.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package cpp
2+
3+
type Config struct {
4+
IncludeDirs []string `yaml:"include_dirs"`
5+
6+
ExcludeSystemHeaders bool `yaml:"exclude_system_headers"`
7+
8+
HeaderExtensions []string `yaml:"header_extensions"`
9+
10+
SourceExtensions []string `yaml:"source_extensions"`
11+
}
12+
13+
func DefaultConfig() *Config {
14+
return &Config{
15+
IncludeDirs: []string{},
16+
ExcludeSystemHeaders: true,
17+
HeaderExtensions: []string{".h", ".hpp", ".hh", ".hxx", ".h++"},
18+
SourceExtensions: []string{".cpp", ".cc", ".cxx", ".c++"},
19+
}
20+
}

internal/cpp/language.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
package cpp
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
9+
"github.com/gabotechs/dep-tree/internal/language"
10+
)
11+
12+
var Extensions = []string{
13+
"cpp", "cc", "cxx", "c++",
14+
"hpp", "hh", "hxx", "h++", "h",
15+
}
16+
17+
type Language struct {
18+
Cfg *Config
19+
}
20+
21+
func NewLanguage(cfg *Config) *Language {
22+
if cfg == nil {
23+
cfg = &Config{}
24+
}
25+
return &Language{Cfg: cfg}
26+
}
27+
28+
func (l *Language) ParseFile(path string) (*language.FileInfo, error) {
29+
content, err := os.ReadFile(path)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
file, err := ParseCppFile(string(content))
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
currentDir, _ := os.Getwd()
40+
relPath, _ := filepath.Rel(currentDir, path)
41+
42+
return &language.FileInfo{
43+
Content: file,
44+
Loc: bytes.Count(content, []byte("\n")),
45+
Size: len(content),
46+
AbsPath: path,
47+
RelPath: relPath,
48+
}, nil
49+
}
50+
51+
func (l *Language) ParseImports(file *language.FileInfo) (*language.ImportsResult, error) {
52+
var result language.ImportsResult
53+
54+
cppFile, ok := file.Content.(*File)
55+
if !ok {
56+
return &result, nil
57+
}
58+
59+
for _, include := range cppFile.Includes {
60+
if include.IsSystem {
61+
continue
62+
}
63+
64+
absPath := l.resolveIncludePath(file.AbsPath, include.Header)
65+
if absPath != "" {
66+
result.Imports = append(result.Imports, language.ImportEntry{
67+
All: true,
68+
AbsPath: absPath,
69+
})
70+
}
71+
}
72+
73+
return &result, nil
74+
}
75+
76+
func (l *Language) ParseExports(file *language.FileInfo) (*language.ExportsResult, error) {
77+
var result language.ExportsResult
78+
79+
// For C++, determining exports is complex as it depends on:
80+
// - Public class/struct members
81+
// - Free functions
82+
// - Global variables
83+
// - Template definitions
84+
// For now, we'll treat header files as exporting everything
85+
// and source files as exporting nothing by default
86+
87+
if l.isHeaderFile(file.AbsPath) {
88+
// Header files export all their content
89+
result.Exports = append(result.Exports, language.ExportEntry{
90+
All: true,
91+
AbsPath: file.AbsPath,
92+
})
93+
}
94+
95+
return &result, nil
96+
}
97+
98+
func (l *Language) resolveIncludePath(sourceFile, includePath string) string {
99+
// If the include path is absolute, return it as-is
100+
if filepath.IsAbs(includePath) {
101+
return includePath
102+
}
103+
104+
// try relative to the source file directory
105+
sourceDir := filepath.Dir(sourceFile)
106+
resolvedPath := filepath.Join(sourceDir, includePath)
107+
108+
if _, err := os.Stat(resolvedPath); err == nil {
109+
abs, _ := filepath.Abs(resolvedPath)
110+
return abs
111+
}
112+
113+
if !hasExtension(includePath) {
114+
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
115+
testPath := resolvedPath + ext
116+
if _, err := os.Stat(testPath); err == nil {
117+
abs, _ := filepath.Abs(testPath)
118+
return abs
119+
}
120+
}
121+
}
122+
123+
// try relative to project root
124+
projectRoots := l.findProjectRoots(sourceDir)
125+
for _, root := range projectRoots {
126+
testPath := filepath.Join(root, includePath)
127+
if _, err := os.Stat(testPath); err == nil {
128+
abs, _ := filepath.Abs(testPath)
129+
return abs
130+
}
131+
132+
// Try with extensions
133+
if !hasExtension(includePath) {
134+
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
135+
testPathWithExt := testPath + ext
136+
if _, err := os.Stat(testPathWithExt); err == nil {
137+
abs, _ := filepath.Abs(testPathWithExt)
138+
return abs
139+
}
140+
}
141+
}
142+
}
143+
144+
// try supporting configured include directories
145+
for _, includeDir := range l.Cfg.IncludeDirs {
146+
var testPath string
147+
if filepath.IsAbs(includeDir) {
148+
testPath = filepath.Join(includeDir, includePath)
149+
} else {
150+
testPath = filepath.Join(sourceDir, includeDir, includePath)
151+
}
152+
153+
if _, err := os.Stat(testPath); err == nil {
154+
abs, _ := filepath.Abs(testPath)
155+
return abs
156+
}
157+
158+
// Try with extensions
159+
if !hasExtension(includePath) {
160+
for _, ext := range []string{".h", ".hpp", ".hxx", ".h++"} {
161+
testPathWithExt := testPath + ext
162+
if _, err := os.Stat(testPathWithExt); err == nil {
163+
abs, _ := filepath.Abs(testPathWithExt)
164+
return abs
165+
}
166+
}
167+
}
168+
}
169+
170+
return ""
171+
}
172+
173+
func (l *Language) findProjectRoots(startDir string) []string {
174+
var roots []string
175+
currentDir := startDir
176+
177+
// Common project root indicators
178+
rootIndicators := []string{
179+
"CMakeLists.txt", "Makefile", "SConstruct", // Build files
180+
".git", ".hg", ".svn", // Version control
181+
"package.json", "Cargo.toml", "go.mod", // Language-specific
182+
"README.md", "README.txt", // Documentation
183+
}
184+
185+
for {
186+
for _, indicator := range rootIndicators {
187+
if _, err := os.Stat(filepath.Join(currentDir, indicator)); err == nil {
188+
roots = append(roots, currentDir)
189+
break
190+
}
191+
}
192+
193+
parentDir := filepath.Dir(currentDir)
194+
if parentDir == currentDir {
195+
break
196+
}
197+
currentDir = parentDir
198+
199+
if len(roots) >= 3 {
200+
break
201+
}
202+
}
203+
204+
return roots
205+
}
206+
207+
func (l *Language) isHeaderFile(path string) bool {
208+
ext := strings.ToLower(filepath.Ext(path))
209+
headerExts := []string{".h", ".hpp", ".hh", ".hxx", ".h++"}
210+
for _, headerExt := range headerExts {
211+
if ext == headerExt {
212+
return true
213+
}
214+
}
215+
return false
216+
}
217+
218+
func hasExtension(path string) bool {
219+
return filepath.Ext(path) != ""
220+
}

0 commit comments

Comments
 (0)