@@ -4,9 +4,11 @@ import (
44 "encoding/json"
55 "flag"
66 "fmt"
7+ "io"
78 "os"
89 "path/filepath"
910 "sort"
11+ "strings"
1012 "time"
1113
1214 "codemap/config"
@@ -248,63 +250,112 @@ func buildProjectContext(root string, info *hubInfo) ProjectContext {
248250 return ctx
249251}
250252
251- // detectLanguagesFromFiles does a quick scan of the project root for language signals.
252- // Checks manifest files first (fast), then scans top-level source files.
253+ // detectLanguagesFromFiles does a quick scan for language signals.
254+ // Checks manifest files first (fast), then scans source files recursively .
253255func detectLanguagesFromFiles (root string ) map [string ]bool {
254256 langs := make (map [string ]bool )
257+ addLang := func (lang string ) {
258+ if lang != "" {
259+ langs [lang ] = true
260+ }
261+ }
255262
256263 // Manifest files → definitive language signal
257- manifests := map [string ]string {
258- "go.mod" : "go" ,
259- "package.json" : "javascript" ,
260- "Cargo.toml" : "rust" ,
261- "pyproject.toml" : "python" ,
262- "setup.py" : "python" ,
263- "requirements.txt" : "python" ,
264- "Gemfile" : "ruby" ,
265- "build.gradle" : "java" ,
266- "pom.xml" : "java" ,
267- "Package.swift" : "swift" ,
268- "mix.exs" : "elixir" ,
269- "composer.json" : "php" ,
270- "build.sbt" : "scala" ,
264+ manifests := map [string ][]string {
265+ "go.mod" : {"go" },
266+ "package.json" : {"javascript" },
267+ "Cargo.toml" : {"rust" },
268+ "pyproject.toml" : {"python" },
269+ "setup.py" : {"python" },
270+ "requirements.txt" : {"python" },
271+ "Gemfile" : {"ruby" },
272+ "build.gradle" : {"java" },
273+ "build.gradle.kts" : {"kotlin" , "java" },
274+ "pom.xml" : {"java" },
275+ "Package.swift" : {"swift" },
276+ "Podfile" : {"swift" },
277+ "mix.exs" : {"elixir" },
278+ "composer.json" : {"php" },
279+ "build.sbt" : {"scala" },
280+ "tsconfig.json" : {"typescript" },
271281 }
272- for file , lang := range manifests {
282+ for file , signalLangs := range manifests {
273283 if _ , err := os .Stat (filepath .Join (root , file )); err == nil {
274- langs [lang ] = true
284+ for _ , lang := range signalLangs {
285+ addLang (lang )
286+ }
275287 }
276288 }
277289
278- // If we found manifests, that's usually enough
279- if len (langs ) > 0 {
280- return langs
290+ // C# project files can have arbitrary names; detect by glob at repo root.
291+ for _ , pattern := range []string {"*.csproj" , "*.sln" } {
292+ matches , _ := filepath .Glob (filepath .Join (root , pattern ))
293+ if len (matches ) > 0 {
294+ addLang ("csharp" )
295+ }
281296 }
282297
283- // Fall back to scanning top-level files by extension
284- entries , err := os .ReadDir (root )
285- if err != nil {
286- return langs
298+ // JS/TS monorepo signal: packages/*/package.json.
299+ if matches , _ := filepath .Glob (filepath .Join (root , "packages" , "*" , "package.json" )); len (matches ) > 0 {
300+ addLang ("javascript" )
287301 }
288- for _ , entry := range entries {
289- if entry .IsDir () {
290- continue
291- }
292- if lang := scanner .DetectLanguage (entry .Name ()); lang != "" {
293- langs [lang ] = true
302+
303+ // Makefile heuristics for C/C++ projects — check directly, no sentinel.
304+ if _ , err := os .Stat (filepath .Join (root , "Makefile" )); err == nil {
305+ applyMakefileHeuristics (filepath .Join (root , "Makefile" ), addLang )
306+ }
307+
308+ // Include subdirectory source files. Reuse the scan result for countSourceFiles too.
309+ gitCache := scanner .NewGitIgnoreCache (root )
310+ if files , err := scanner .ScanFiles (root , gitCache , nil , nil ); err == nil {
311+ for _ , f := range files {
312+ addLang (scanner .DetectLanguage (f .Path ))
294313 }
314+ // Cache file count to avoid a second scan in countSourceFiles
315+ cachedFileCount = len (files )
295316 }
296317
297318 return langs
298319}
299320
321+ // cachedFileCount avoids a second ScanFiles walk in countSourceFiles.
322+ var cachedFileCount = - 1
323+
324+ func applyMakefileHeuristics (path string , addLang func (string )) {
325+ f , err := os .Open (path )
326+ if err != nil {
327+ return
328+ }
329+ defer f .Close ()
330+
331+ buf , err := io .ReadAll (io .LimitReader (f , 128 * 1024 ))
332+ if err != nil {
333+ return
334+ }
335+ content := strings .ToLower (string (buf ))
336+
337+ if strings .Contains (content , "g++" ) || strings .Contains (content , "clang++" ) || strings .Contains (content , ".cpp" ) || strings .Contains (content , ".cc" ) {
338+ addLang ("cpp" )
339+ }
340+ // Tighten C detection: exclude clang++ and .cpp/.cc false positives
341+ if strings .Contains (content , "gcc" ) ||
342+ (strings .Contains (content , "clang" ) && ! strings .Contains (content , "clang++" )) {
343+ addLang ("c" )
344+ }
345+ }
346+
300347// countSourceFiles does a quick count of source files in the project.
348+ // Uses cached result from detectLanguagesFromFiles if available.
301349func countSourceFiles (root string ) int {
302- count := 0
350+ if cachedFileCount >= 0 {
351+ count := cachedFileCount
352+ cachedFileCount = - 1 // reset for next call
353+ return count
354+ }
303355 gitCache := scanner .NewGitIgnoreCache (root )
304356 files , err := scanner .ScanFiles (root , gitCache , nil , nil )
305357 if err != nil {
306358 return 0
307359 }
308- count = len (files )
309- return count
360+ return len (files )
310361}
0 commit comments