@@ -304,6 +304,19 @@ async function detectPackageManagers(root: string): Promise<string[]> {
304304 ) {
305305 found . push ( "gradle" ) ;
306306 }
307+ if (
308+ ! found . includes ( "cmake" ) &&
309+ ( await containsFileNamed ( root , "CMakeLists.txt" , 5 , shouldSkipCOrCppSearchEntry ) )
310+ ) {
311+ found . push ( "cmake" ) ;
312+ }
313+ if (
314+ ! found . includes ( "autotools" ) &&
315+ ( ( await containsFileNamed ( root , "Makefile.am" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
316+ ( await containsFileNamed ( root , "Makefile.in" , 5 , shouldSkipCOrCppSearchEntry ) ) )
317+ ) {
318+ found . push ( "autotools" ) ;
319+ }
307320 if ( await pathExists ( join ( root , "composer.json" ) ) ) {
308321 found . push ( "composer" ) ;
309322 }
@@ -978,12 +991,35 @@ async function detectLanguages(root: string): Promise<string[]> {
978991 ) {
979992 languages . push ( "kotlin" ) ;
980993 }
994+ if ( ! languages . includes ( "c" ) && ( await containsCFile ( root ) ) ) {
995+ languages . push ( "c" ) ;
996+ }
997+ if ( ! languages . includes ( "cpp" ) && ( await containsCppFile ( root ) ) ) {
998+ languages . push ( "cpp" ) ;
999+ }
9811000 if ( ! languages . includes ( "php" ) && ( await containsReviewablePhpFile ( root ) ) ) {
9821001 languages . push ( "php" ) ;
9831002 }
9841003 return languages ;
9851004}
9861005
1006+ async function containsCFile ( root : string ) : Promise < boolean > {
1007+ return containsFileWithExtension ( root , ".c" , 5 , shouldSkipCOrCppSearchEntry ) ;
1008+ }
1009+
1010+ async function containsCppFile ( root : string ) : Promise < boolean > {
1011+ return (
1012+ ( await containsFileWithExtension ( root , ".C" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1013+ ( await containsFileWithExtension ( root , ".H" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1014+ ( await containsFileWithExtensionIgnoringCase ( root , ".cpp" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1015+ ( await containsFileWithExtensionIgnoringCase ( root , ".cc" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1016+ ( await containsFileWithExtensionIgnoringCase ( root , ".cxx" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1017+ ( await containsFileWithExtensionIgnoringCase ( root , ".hpp" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1018+ ( await containsFileWithExtensionIgnoringCase ( root , ".hh" , 5 , shouldSkipCOrCppSearchEntry ) ) ||
1019+ ( await containsFileWithExtensionIgnoringCase ( root , ".hxx" , 5 , shouldSkipCOrCppSearchEntry ) )
1020+ ) ;
1021+ }
1022+
9871023const jvmSourceSearchRoots = [ "src" , "app" , "apps" , "lib" ] as const ;
9881024
9891025async function containsReviewableJavaFile ( root : string ) : Promise < boolean > {
@@ -996,7 +1032,7 @@ async function containsReviewableKotlinFile(root: string): Promise<boolean> {
9961032
9971033async function containsReviewableJvmFile ( root : string , extension : string ) : Promise < boolean > {
9981034 for ( const prefix of jvmSourceSearchRoots ) {
999- if ( await containsFileWithExtension ( join ( root , prefix ) , extension , 8 ) ) {
1035+ if ( await containsFileWithExtension ( join ( root , prefix ) , extension , 8 , undefined , prefix ) ) {
10001036 return true ;
10011037 }
10021038 }
@@ -1031,7 +1067,15 @@ async function containsReviewablePythonFile(root: string): Promise<boolean> {
10311067 return true ;
10321068 }
10331069 for ( const prefix of pythonSourceSearchRoots ) {
1034- if ( await containsFileMatching ( join ( root , prefix ) , 4 , isReviewablePythonFileName ) ) {
1070+ if (
1071+ await containsFileMatching (
1072+ join ( root , prefix ) ,
1073+ 4 ,
1074+ isReviewablePythonFileName ,
1075+ undefined ,
1076+ prefix ,
1077+ )
1078+ ) {
10351079 return true ;
10361080 }
10371081 }
@@ -1040,7 +1084,7 @@ async function containsReviewablePythonFile(root: string): Promise<boolean> {
10401084
10411085async function containsReviewablePhpFile ( root : string ) : Promise < boolean > {
10421086 for ( const prefix of [ "app" , "routes" , "config" , "database" , "tests" ] ) {
1043- if ( await containsFileWithExtension ( join ( root , prefix ) , ".php" , 4 ) ) {
1087+ if ( await containsFileWithExtension ( join ( root , prefix ) , ".php" , 4 , undefined , prefix ) ) {
10441088 return true ;
10451089 }
10461090 }
@@ -1089,7 +1133,7 @@ async function pythonFrameworkScanFiles(root: string): Promise<string[]> {
10891133 }
10901134 }
10911135 for ( const prefix of pythonSourceSearchRoots ) {
1092- await collectPythonFrameworkScanFiles ( join ( root , prefix ) , 4 , files ) ;
1136+ await collectPythonFrameworkScanFiles ( join ( root , prefix ) , 4 , files , prefix ) ;
10931137 }
10941138 return [ ...new Set ( files ) ] . slice ( 0 , 200 ) ;
10951139}
@@ -1098,6 +1142,7 @@ async function collectPythonFrameworkScanFiles(
10981142 dir : string ,
10991143 remainingDepth : number ,
11001144 files : string [ ] ,
1145+ relativeDir = "" ,
11011146) : Promise < void > {
11021147 if ( remainingDepth < 0 || ! ( await pathExists ( dir ) ) ) {
11031148 return ;
@@ -1107,7 +1152,8 @@ async function collectPythonFrameworkScanFiles(
11071152 return ;
11081153 }
11091154 for ( const entry of await readdir ( dir , { withFileTypes : true } ) ) {
1110- if ( shouldSkipSearchEntry ( entry . name ) ) {
1155+ const path = relativeDir === "" ? entry . name : `${ relativeDir } /${ entry . name } ` ;
1156+ if ( shouldSkipSearchEntry ( entry . name , path ) ) {
11111157 continue ;
11121158 }
11131159 const full = join ( dir , entry . name ) ;
@@ -1117,7 +1163,7 @@ async function collectPythonFrameworkScanFiles(
11171163 if ( entry . isFile ( ) && isReviewablePythonFileName ( entry . name ) ) {
11181164 files . push ( full ) ;
11191165 } else if ( entry . isDirectory ( ) ) {
1120- await collectPythonFrameworkScanFiles ( full , remainingDepth - 1 , files ) ;
1166+ await collectPythonFrameworkScanFiles ( full , remainingDepth - 1 , files , path ) ;
11211167 }
11221168 }
11231169}
@@ -1127,12 +1173,14 @@ async function containsReviewableRubyFile(root: string): Promise<boolean> {
11271173 return true ;
11281174 }
11291175 for ( const prefix of [ "app" , "lib" ] ) {
1130- if ( await containsFileMatching ( join ( root , prefix ) , 4 , isReviewableRubyFileName ) ) {
1176+ if (
1177+ await containsFileMatching ( join ( root , prefix ) , 4 , isReviewableRubyFileName , undefined , prefix )
1178+ ) {
11311179 return true ;
11321180 }
11331181 }
11341182 for ( const prefix of [ "scripts" , "script" , "exe" , "bin" ] ) {
1135- if ( await containsRubyExecutableSource ( join ( root , prefix ) , 4 ) ) {
1183+ if ( await containsRubyExecutableSource ( join ( root , prefix ) , 4 , prefix ) ) {
11361184 return true ;
11371185 }
11381186 }
@@ -1152,7 +1200,11 @@ function isRootReviewableRubyFileName(entry: string): boolean {
11521200 return isReviewableRubyFileName ( entry ) && ! entry . startsWith ( "test_" ) ;
11531201}
11541202
1155- async function containsRubyExecutableSource ( dir : string , remainingDepth : number ) : Promise < boolean > {
1203+ async function containsRubyExecutableSource (
1204+ dir : string ,
1205+ remainingDepth : number ,
1206+ relativeDir = "" ,
1207+ ) : Promise < boolean > {
11561208 if ( remainingDepth < 0 || ! ( await pathExists ( dir ) ) ) {
11571209 return false ;
11581210 }
@@ -1161,7 +1213,8 @@ async function containsRubyExecutableSource(dir: string, remainingDepth: number)
11611213 return false ;
11621214 }
11631215 for ( const entry of await readdir ( dir ) ) {
1164- if ( shouldSkipSearchEntry ( entry ) ) {
1216+ const path = relativeDir === "" ? entry : `${ relativeDir } /${ entry } ` ;
1217+ if ( shouldSkipSearchEntry ( entry , path ) ) {
11651218 continue ;
11661219 }
11671220 const full = join ( dir , entry ) ;
@@ -1176,7 +1229,10 @@ async function containsRubyExecutableSource(dir: string, remainingDepth: number)
11761229 ) {
11771230 return true ;
11781231 }
1179- if ( info . isDirectory ( ) && ( await containsRubyExecutableSource ( full , remainingDepth - 1 ) ) ) {
1232+ if (
1233+ info . isDirectory ( ) &&
1234+ ( await containsRubyExecutableSource ( full , remainingDepth - 1 , path ) )
1235+ ) {
11801236 return true ;
11811237 }
11821238 }
@@ -1241,22 +1297,54 @@ async function containsRubyPrefixedMinitestFile(
12411297 return false ;
12421298}
12431299
1244- async function containsFileNamed ( root : string , name : string , maxDepth : number ) : Promise < boolean > {
1245- return containsFileMatching ( root , maxDepth , ( entry ) => entry === name ) ;
1300+ async function containsFileNamed (
1301+ root : string ,
1302+ name : string ,
1303+ maxDepth : number ,
1304+ skipEntry : ( entry : string , relativePath : string ) => boolean = shouldSkipSearchEntry ,
1305+ ) : Promise < boolean > {
1306+ return containsFileMatching ( root , maxDepth , ( entry ) => entry === name , skipEntry ) ;
12461307}
12471308
12481309async function containsFileWithExtension (
12491310 root : string ,
12501311 extension : string ,
12511312 maxDepth : number ,
1313+ skipEntry : ( entry : string , relativePath : string ) => boolean = shouldSkipSearchEntry ,
1314+ relativeDir = "" ,
12521315) : Promise < boolean > {
1253- return containsFileMatching ( root , maxDepth , ( entry ) => entry . endsWith ( extension ) ) ;
1316+ return containsFileMatching (
1317+ root ,
1318+ maxDepth ,
1319+ ( entry ) => entry . endsWith ( extension ) ,
1320+ skipEntry ,
1321+ relativeDir ,
1322+ ) ;
1323+ }
1324+
1325+ async function containsFileWithExtensionIgnoringCase (
1326+ root : string ,
1327+ extension : string ,
1328+ maxDepth : number ,
1329+ skipEntry : ( entry : string , relativePath : string ) => boolean = shouldSkipSearchEntry ,
1330+ relativeDir = "" ,
1331+ ) : Promise < boolean > {
1332+ const lowercaseExtension = extension . toLowerCase ( ) ;
1333+ return containsFileMatching (
1334+ root ,
1335+ maxDepth ,
1336+ ( entry ) => entry . toLowerCase ( ) . endsWith ( lowercaseExtension ) ,
1337+ skipEntry ,
1338+ relativeDir ,
1339+ ) ;
12541340}
12551341
12561342async function containsFileMatching (
12571343 dir : string ,
12581344 remainingDepth : number ,
12591345 predicate : ( entry : string ) => boolean ,
1346+ skipEntry : ( entry : string , relativePath : string ) => boolean = shouldSkipSearchEntry ,
1347+ relativeDir = "" ,
12601348) : Promise < boolean > {
12611349 if ( remainingDepth < 0 || ! ( await pathExists ( dir ) ) ) {
12621350 return false ;
@@ -1266,7 +1354,8 @@ async function containsFileMatching(
12661354 return false ;
12671355 }
12681356 for ( const entry of await readdir ( dir ) ) {
1269- if ( shouldSkipSearchEntry ( entry ) ) {
1357+ const relativePath = relativeDir . length === 0 ? entry : `${ relativeDir } /${ entry } ` ;
1358+ if ( skipEntry ( entry , relativePath ) ) {
12701359 continue ;
12711360 }
12721361 const full = join ( dir , entry ) ;
@@ -1277,14 +1366,20 @@ async function containsFileMatching(
12771366 if ( info . isFile ( ) && predicate ( entry ) ) {
12781367 return true ;
12791368 }
1280- if ( info . isDirectory ( ) && ( await containsFileMatching ( full , remainingDepth - 1 , predicate ) ) ) {
1369+ if (
1370+ info . isDirectory ( ) &&
1371+ ( await containsFileMatching ( full , remainingDepth - 1 , predicate , skipEntry , relativePath ) )
1372+ ) {
12811373 return true ;
12821374 }
12831375 }
12841376 return false ;
12851377}
12861378
1287- function shouldSkipSearchEntry ( entry : string ) : boolean {
1379+ function shouldSkipSearchEntry ( entry : string , relativePath = entry ) : boolean {
1380+ if ( entry === "vendor" && relativePath === "vendor" ) {
1381+ return true ;
1382+ }
12881383 return [
12891384 "node_modules" ,
12901385 "dist" ,
@@ -1302,7 +1397,6 @@ function shouldSkipSearchEntry(entry: string): boolean {
13021397 ".ruff_cache" ,
13031398 ".pytest_cache" ,
13041399 ".bundle" ,
1305- "vendor" ,
13061400 "fixtures" ,
13071401 "__fixtures__" ,
13081402 "testdata" ,
@@ -1313,6 +1407,15 @@ function shouldSkipSearchEntry(entry: string): boolean {
13131407 ] . includes ( entry ) ;
13141408}
13151409
1410+ function shouldSkipCOrCppSearchEntry ( entry : string ) : boolean {
1411+ return (
1412+ shouldSkipSearchEntry ( entry ) ||
1413+ entry === "vendor" ||
1414+ entry === "CMakeFiles" ||
1415+ / ^ c m a k e - b u i l d - [ ^ / ] + $ / u. test ( entry )
1416+ ) ;
1417+ }
1418+
13161419function stripLineComments ( source : string , marker : "//" ) : string {
13171420 return source
13181421 . split ( "\n" )
0 commit comments