Skip to content

Commit bc82815

Browse files
committed
Fixes CDS extractor project-aware file detection
Fixes detection of .cds file in CDS projects by ensuring that "node_modules" subdirectories are explicitly ignored and "srv" and "db" subdirectories are explicitly included. Migrates some logic from cds-extractor.ts (entrypoint) script to testable functions under extractors/cds/tools/src/ directory. Adds and improves unit tests related to code changes from this commit.
1 parent d6a99da commit bc82815

File tree

7 files changed

+1078
-342
lines changed

7 files changed

+1078
-342
lines changed

extractors/cds/tools/cds-extractor.ts

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { CdsProjectMapWithDebugSignals } from './src/cds/parser/types';
88
import { runJavaScriptExtractor } from './src/codeql';
99
import { addCompilationDiagnostic } from './src/diagnostics';
1010
import { configureLgtmIndexFilters, setupAndValidateEnvironment } from './src/environment';
11-
import { getCdsFilePathsToProcess } from './src/filesystem';
11+
import { handleIndexFilesMode } from './src/indexFiles';
1212
import { installDependencies } from './src/packageManager';
1313
import { RunMode, validateArguments } from './src/utils';
1414

@@ -60,21 +60,6 @@ console.log(
6060
`INFO: CodeQL CDS extractor using run mode '${runMode}' for scan of project source root directory '${sourceRoot}'.`,
6161
);
6262

63-
// Only process response file for INDEX_FILES mode
64-
let cdsFilePathsToProcess: string[] = [];
65-
if (runMode === (RunMode.INDEX_FILES as string)) {
66-
// Validate response file and get the full paths of CDS files to process.
67-
const filePathsResult = getCdsFilePathsToProcess(responseFile, platformInfo);
68-
if (!filePathsResult.success) {
69-
console.warn(filePathsResult.errorMessage);
70-
// Exit with an error if unable to get a list of `.cds` file paths to process.
71-
process.exit(1);
72-
}
73-
74-
// Get the validated list of CDS files to process
75-
cdsFilePathsToProcess = filePathsResult.cdsFilePaths;
76-
}
77-
7863
// Using the new project-aware approach to find CDS projects and their dependencies
7964
console.log('Detecting CDS projects and analyzing their structure...');
8065

@@ -98,6 +83,29 @@ if (typedProjectMap.__debugParserSuccess) {
9883
console.log('Installing required CDS compiler versions using cached approach...');
9984
const projectCacheDirMap = installDependencies(projectMap, sourceRoot, codeqlExePath);
10085

86+
// Handle run mode specific logic
87+
let cdsFilePathsToProcess: string[] = [];
88+
89+
if (runMode === (RunMode.INDEX_FILES as string)) {
90+
// Use the dedicated function for INDEX_FILES mode
91+
const indexFilesResult = handleIndexFilesMode(projectMap, sourceRoot, responseFile, platformInfo);
92+
93+
if (!indexFilesResult.validationResult.success) {
94+
console.warn(indexFilesResult.validationResult.errorMessage);
95+
// Exit with an error if unable to read the response file
96+
process.exit(1);
97+
}
98+
99+
cdsFilePathsToProcess = indexFilesResult.cdsFilePathsToProcess;
100+
} else {
101+
console.log('Extracting CDS files from discovered projects...');
102+
103+
// Extract all CDS files from the discovered projects for other run modes
104+
for (const [, project] of projectMap.entries()) {
105+
cdsFilePathsToProcess.push(...project.cdsFiles);
106+
}
107+
}
108+
101109
console.log('Processing CDS files to JSON ...');
102110

103111
// Compile each `.cds` file to create a `.cds.json` file.

extractors/cds/tools/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"clean": "rm -rf out coverage",
1010
"prebuild": "npm run clean",
1111
"lint": "eslint --ext .ts src/",
12-
"lint:fix": "eslint --ext .ts --fix cds-extractor.ts src/",
12+
"lint:fix": "eslint --ext .ts --fix cds-extractor.ts src/ test/src/",
1313
"format": "prettier --write 'src/**/*.ts'",
1414
"test": "jest",
1515
"test:watch": "jest --watch",

extractors/cds/tools/src/cds/parser/functions.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,11 @@ export function determineCdsFilesForProjectDir(
197197
}
198198

199199
try {
200-
// Use glob to find all .cds files under the project directory
201-
const cdsFiles = sync(join(projectDir, '**/*.cds'), { nodir: true });
200+
// Use glob to find all .cds files under the project directory, excluding node_modules
201+
const cdsFiles = sync(join(projectDir, '**/*.cds'), {
202+
nodir: true,
203+
ignore: ['**/node_modules/**'],
204+
});
202205

203206
// Convert absolute paths to paths relative to sourceRootDir
204207
return cdsFiles.map(file => relative(sourceRootDir, file));
@@ -223,8 +226,11 @@ export function determineCdsProjectsUnderSourceDir(sourceRootDir: string): strin
223226
const projectDirs: string[] = [];
224227
const processedDirectories = new Set<string>();
225228

226-
// Find all package.json files under the source directory
227-
const packageJsonFiles = sync(join(sourceRootDir, '**/package.json'), { nodir: true });
229+
// Find all package.json files under the source directory, excluding node_modules
230+
const packageJsonFiles = sync(join(sourceRootDir, '**/package.json'), {
231+
nodir: true,
232+
ignore: ['**/node_modules/**'],
233+
});
228234

229235
// Check each directory containing a package.json file
230236
for (const packageJsonFile of packageJsonFiles) {
@@ -244,8 +250,11 @@ export function determineCdsProjectsUnderSourceDir(sourceRootDir: string): strin
244250
}
245251
}
246252

247-
// Also check directories that have .cds files but no package.json
248-
const cdsFiles = sync(join(sourceRootDir, '**/*.cds'), { nodir: true });
253+
// Also check directories that have .cds files but no package.json, excluding node_modules
254+
const cdsFiles = sync(join(sourceRootDir, '**/*.cds'), {
255+
nodir: true,
256+
ignore: ['**/node_modules/**'],
257+
});
249258
const cdsDirsSet = new Set(cdsFiles.map(file => dirname(file)));
250259
const cdsDirs = Array.from(cdsDirsSet);
251260

@@ -279,6 +288,11 @@ export function determineCdsProjectsUnderSourceDir(sourceRootDir: string): strin
279288
* @returns true if the directory or a parent has been processed
280289
*/
281290
export function isDirectoryProcessed(dir: string, processedDirectories: Set<string>): boolean {
291+
// Skip node_modules directories entirely
292+
if (dir.includes('node_modules')) {
293+
return true; // Consider node_modules as already processed to skip them
294+
}
295+
282296
let currentDir = dir;
283297

284298
while (currentDir) {
@@ -432,6 +446,11 @@ export function findProjectRootFromCdsFile(
432446
cdsFileDir: string,
433447
sourceRootDir: string,
434448
): string | null {
449+
// Skip node_modules directories entirely
450+
if (cdsFileDir.includes('node_modules')) {
451+
return null;
452+
}
453+
435454
let currentDir = cdsFileDir;
436455

437456
// Limit the upward search to the sourceRootDir
@@ -442,7 +461,11 @@ export function findProjectRootFromCdsFile(
442461
// be the real project root containing both db and srv directories
443462
const parentDir = dirname(currentDir);
444463

445-
if (parentDir !== currentDir && parentDir.startsWith(sourceRootDir)) {
464+
if (
465+
parentDir !== currentDir &&
466+
parentDir.startsWith(sourceRootDir) &&
467+
!parentDir.includes('node_modules')
468+
) {
446469
const hasDbDir =
447470
existsSync(join(parentDir, 'db')) && statSync(join(parentDir, 'db')).isDirectory();
448471
const hasSrvDir =
@@ -522,6 +545,11 @@ export function getAllCdsFiles(sourceRootDir: string): { filePath: string; proje
522545
*/
523546
export function isLikelyCdsProject(dir: string): boolean {
524547
try {
548+
// Skip node_modules directories entirely
549+
if (dir.includes('node_modules')) {
550+
return false;
551+
}
552+
525553
// Check if package.json exists and has CAP dependencies
526554
const packageJsonPath = join(dir, 'package.json');
527555
const packageJson = readPackageJsonWithCache(packageJsonPath);
@@ -538,12 +566,13 @@ export function isLikelyCdsProject(dir: string): boolean {
538566
}
539567
}
540568

541-
// Check for CDS files in standard locations
569+
// Check for CDS files in standard locations (checking both direct and nested files)
542570
const standardLocations = [join(dir, 'db'), join(dir, 'srv'), join(dir, 'app')];
543571

544572
for (const location of standardLocations) {
545573
if (existsSync(location) && statSync(location).isDirectory()) {
546-
const cdsFiles = sync(join(location, '*.cds'));
574+
// Check for any .cds files at any level under these directories
575+
const cdsFiles = sync(join(location, '**/*.cds'), { nodir: true });
547576
if (cdsFiles.length > 0) {
548577
return true;
549578
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { join } from 'path';
2+
3+
import { CdsProject } from './cds/parser/types';
4+
import { getCdsFilePathsToProcess } from './filesystem';
5+
6+
export interface IndexFilesValidationResult {
7+
success: boolean;
8+
warnings: string[];
9+
errorMessage?: string;
10+
discoveredCount: number;
11+
responseFileCount: number;
12+
}
13+
14+
/**
15+
* Validates discovered CDS files against the response file for INDEX_FILES mode.
16+
* This function performs backwards compatibility validation to ensure that the
17+
* project-aware discovery is consistent with the legacy response file approach.
18+
*
19+
* @param cdsFilePathsToProcess Array of CDS file paths discovered through project-aware discovery
20+
* @param sourceRoot The source root directory
21+
* @param responseFile Path to the response file
22+
* @param platformInfo Platform information object with isWindows property
23+
* @returns Validation result with warnings and statistics
24+
*/
25+
export function validateIndexFilesMode(
26+
cdsFilePathsToProcess: string[],
27+
sourceRoot: string,
28+
responseFile: string,
29+
platformInfo: { isWindows: boolean },
30+
): IndexFilesValidationResult {
31+
const warnings: string[] = [];
32+
33+
// Validate response file and get the full paths of CDS files from response file
34+
const responseFileResult = getCdsFilePathsToProcess(responseFile, platformInfo);
35+
if (!responseFileResult.success) {
36+
return {
37+
success: false,
38+
warnings: [],
39+
errorMessage: responseFileResult.errorMessage,
40+
discoveredCount: cdsFilePathsToProcess.length,
41+
responseFileCount: 0,
42+
};
43+
}
44+
45+
const responseFilePaths = responseFileResult.cdsFilePaths;
46+
47+
// Convert discovered files to absolute paths for comparison
48+
const discoveredAbsolutePaths = cdsFilePathsToProcess.map(relativePath =>
49+
relativePath.startsWith(sourceRoot) ? relativePath : join(sourceRoot, relativePath),
50+
);
51+
52+
// Validate that discovered files are consistent with response file
53+
const responseFileBasenames = responseFilePaths.map(path => path.replace(/^.*[/\\]/, ''));
54+
const discoveredBasenames = discoveredAbsolutePaths.map(path => path.replace(/^.*[/\\]/, ''));
55+
56+
const unexpectedFiles = discoveredBasenames.filter(
57+
basename => !responseFileBasenames.includes(basename),
58+
);
59+
const missingFiles = responseFileBasenames.filter(
60+
basename => !discoveredBasenames.includes(basename),
61+
);
62+
63+
if (unexpectedFiles.length > 0) {
64+
warnings.push(`Discovered CDS files not in response file: ${unexpectedFiles.join(', ')}`);
65+
}
66+
67+
if (missingFiles.length > 0) {
68+
warnings.push(`Response file contains CDS files not discovered: ${missingFiles.join(', ')}`);
69+
}
70+
71+
return {
72+
success: true,
73+
warnings,
74+
discoveredCount: cdsFilePathsToProcess.length,
75+
responseFileCount: responseFilePaths.length,
76+
};
77+
}
78+
79+
/**
80+
* Handles the INDEX_FILES run mode processing.
81+
* This function performs project-aware CDS file discovery and validates the results
82+
* against the response file for backwards compatibility.
83+
*
84+
* @param projectMap Map of CDS projects discovered through project-aware parsing
85+
* @param sourceRoot The source root directory
86+
* @param responseFile Path to the response file
87+
* @param platformInfo Platform information object with isWindows property
88+
* @returns Object containing the CDS file paths to process and validation result
89+
*/
90+
export function handleIndexFilesMode(
91+
projectMap: Map<string, CdsProject>,
92+
sourceRoot: string,
93+
responseFile: string,
94+
platformInfo: { isWindows: boolean },
95+
): {
96+
cdsFilePathsToProcess: string[];
97+
validationResult: IndexFilesValidationResult;
98+
} {
99+
console.log('Extracting CDS files from discovered projects...');
100+
101+
// Extract all CDS files from the discovered projects
102+
const cdsFilePathsToProcess: string[] = [];
103+
for (const [, project] of projectMap.entries()) {
104+
cdsFilePathsToProcess.push(...project.cdsFiles);
105+
}
106+
107+
console.log(
108+
'Validating discovered CDS files against response file for backwards compatibility...',
109+
);
110+
111+
// Validate discovered files against response file
112+
const validationResult = validateIndexFilesMode(
113+
cdsFilePathsToProcess,
114+
sourceRoot,
115+
responseFile,
116+
platformInfo,
117+
);
118+
119+
if (!validationResult.success) {
120+
return {
121+
cdsFilePathsToProcess: [],
122+
validationResult,
123+
};
124+
}
125+
126+
// Log warnings if any
127+
validationResult.warnings.forEach(warning => {
128+
console.warn(`Warning: ${warning}`);
129+
});
130+
131+
console.log(`Discovered ${validationResult.discoveredCount} CDS files from project analysis`);
132+
console.log(`Response file specified ${validationResult.responseFileCount} CDS files`);
133+
134+
return {
135+
cdsFilePathsToProcess,
136+
validationResult,
137+
};
138+
}

0 commit comments

Comments
 (0)