Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5,052 changes: 4,801 additions & 251 deletions extractors/cds/tools/dist/cds-extractor.bundle.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions extractors/cds/tools/dist/cds-extractor.bundle.js.map

Large diffs are not rendered by default.

4,496 changes: 4,450 additions & 46 deletions extractors/cds/tools/dist/compile-test-cds-lib.cjs

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions extractors/cds/tools/dist/compile-test-cds-lib.cjs.map

Large diffs are not rendered by default.

1,165 changes: 607 additions & 558 deletions extractors/cds/tools/package-lock.json

Large diffs are not rendered by default.

24 changes: 14 additions & 10 deletions extractors/cds/tools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,36 @@
},
"dependencies": {
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"fs": "^0.0.2",
"glob": "^13.0.6",
"js-yaml": "^4.1.1",
"minimatch": "^10.2.4",
Comment thread
data-douser marked this conversation as resolved.
"os": "^0.1.2",
"path": "^0.12.7",
"tmp": "^0.2.5"
},
"devDependencies": {
"@eslint/compat": "^2.0.2",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@eslint/compat": "^2.0.3",
"@eslint/eslintrc": "^3.3.5",
"@eslint/js": "^9.39.4",
"@types/glob": "^9.0.0",
"@types/jest": "^30.0.0",
"@types/js-yaml": "^4.0.9",
"@types/minimatch": "^5.1.2",
"@types/mock-fs": "^4.13.4",
"@types/node": "^25.2.3",
"@types/node": "^25.4.0",
"@types/tmp": "^0.2.6",
"@typescript-eslint/eslint-plugin": "^8.55.0",
"@typescript-eslint/parser": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^8.57.0",
"@typescript-eslint/parser": "^8.57.0",
"esbuild": "^0.27.3",
"eslint": "^9.39.2",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.14.0",
"eslint-plugin-jest": "^29.15.0",
"eslint-plugin-prettier": "^5.5.5",
"globals": "^16.5.0",
"jest": "^30.2.0",
"jest": "^30.3.0",
"mock-fs": "^5.5.0",
"prettier": "^3.8.1",
"ts-jest": "^29.4.6",
Expand Down
19 changes: 18 additions & 1 deletion extractors/cds/tools/src/cds/parser/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { sync } from 'glob';
import { CdsFilesToCompile, CdsImport, PackageJson } from './types';
import { modelCdsJsonFile } from '../../constants';
import { cdsExtractorLog } from '../../logging';
import { filterIgnoredPaths, getPathsIgnorePatterns } from '../../paths-ignore';

/**
* Determines the list of CDS files to be parsed for the specified project directory.
Expand Down Expand Up @@ -49,7 +50,23 @@ export function determineCdsFilesForProjectDir(
});

// Convert absolute paths to paths relative to sourceRootDir
return cdsFiles.map(file => relative(sourceRootDir, file));
const relativePaths = cdsFiles.map(file => relative(sourceRootDir, file));

// Apply paths-ignore filtering from CodeQL config
const pathsIgnorePatterns = getPathsIgnorePatterns(sourceRootDir);
if (pathsIgnorePatterns.length > 0) {
const filtered = filterIgnoredPaths(relativePaths, pathsIgnorePatterns);
const ignoredCount = relativePaths.length - filtered.length;
if (ignoredCount > 0) {
cdsExtractorLog(
'info',
`Filtered ${ignoredCount} CDS file(s) matching paths-ignore patterns in project ${relative(sourceRootDir, projectDir) || '.'}`,
);
}
return filtered;
}

return relativePaths;
} catch (error: unknown) {
cdsExtractorLog('error', `Error finding CDS files in ${projectDir}: ${String(error)}`);
return [];
Expand Down
19 changes: 19 additions & 0 deletions extractors/cds/tools/src/cds/parser/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { CdsDependencyGraph, CdsImport, CdsProject, BasicCdsProject } from './types';
import { modelCdsJsonFile } from '../../constants';
import { cdsExtractorLog } from '../../logging';
import { getPathsIgnorePatterns, shouldIgnorePath } from '../../paths-ignore';

/**
* Builds a basic dependency graph of CDS projects and performs the initial parsing stage of the CDS extractor.
Expand All @@ -30,13 +31,31 @@ function buildBasicCdsProjectDependencyGraph(sourceRootDir: string): Map<string,

cdsExtractorLog('info', `Found ${projectDirs.length} CDS project(s) under source directory.`);

// Load paths-ignore patterns once for the entire source root
const pathsIgnorePatterns = getPathsIgnorePatterns(sourceRootDir);

const projectMap = new Map<string, BasicCdsProject>();

// First pass: create CdsProject objects for each project directory
for (const projectDir of projectDirs) {
// Skip projects whose directory matches a paths-ignore pattern
if (pathsIgnorePatterns.length > 0 && shouldIgnorePath(projectDir, pathsIgnorePatterns)) {
cdsExtractorLog('info', `Skipping project '${projectDir}' — matches paths-ignore pattern`);
continue;
}

const absoluteProjectDir = join(sourceRootDir, projectDir);
const cdsFiles = determineCdsFilesForProjectDir(sourceRootDir, absoluteProjectDir);

// Skip projects with no CDS files remaining after paths-ignore filtering
if (cdsFiles.length === 0) {
cdsExtractorLog(
'info',
`Skipping project '${projectDir}' — no CDS files remain after paths-ignore filtering`,
);
continue;
}

// Try to load package.json if it exists
const packageJsonPath = join(absoluteProjectDir, 'package.json');
const packageJson = readPackageJsonFile(packageJsonPath);
Expand Down
5 changes: 4 additions & 1 deletion extractors/cds/tools/src/codeql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { spawnSync, SpawnSyncReturns } from 'child_process';

import type { CdsDependencyGraph } from './cds/parser';
import { addJavaScriptExtractorDiagnostic } from './diagnostics';
import { configureLgtmIndexFilters } from './environment';
import { applyPathsIgnoreToLgtmFilters, configureLgtmIndexFilters } from './environment';
import { createMarkerFile, removeMarkerFile } from './filesystem';
import {
cdsExtractorLog,
Expand Down Expand Up @@ -92,6 +92,9 @@ export function runJavaScriptExtractionWithMarker(
// Configure LGTM index filters
configureLgtmIndexFilters();

// Apply paths-ignore patterns from CodeQL config to LGTM_INDEX_FILTERS
applyPathsIgnoreToLgtmFilters(sourceRoot);

// Create marker file
const markerFilePath = createMarkerFile(sourceRoot);

Expand Down
25 changes: 25 additions & 0 deletions extractors/cds/tools/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join, resolve } from 'path';
import { cdsExtractorMarkerFileName } from './constants';
import { dirExists } from './filesystem';
import { cdsExtractorLog } from './logging';
import { getPathsIgnorePatterns } from './paths-ignore';

/**
* Interface for platform information
Expand Down Expand Up @@ -258,6 +259,30 @@ ${process.env.LGTM_INDEX_FILTERS}`,
process.env.LGTM_INDEX_FILETYPES = '.cds:JSON';
}

/**
* Applies paths-ignore patterns from the CodeQL configuration to the
* LGTM_INDEX_FILTERS environment variable. This ensures the JavaScript
* extractor also respects the user's paths-ignore configuration for
* compiled .cds.json output files.
*
* @param sourceRoot - The source root directory used to locate the config file
*/
export function applyPathsIgnoreToLgtmFilters(sourceRoot: string): void {
const patterns = getPathsIgnorePatterns(sourceRoot);
if (patterns.length === 0) {
return;
}

const excludeLines = patterns.map(p => `exclude:${p}`).join('\n');
const current = process.env.LGTM_INDEX_FILTERS ?? '';
process.env.LGTM_INDEX_FILTERS = current + '\n' + excludeLines;

cdsExtractorLog(
'info',
`Applied ${patterns.length} paths-ignore pattern(s) to LGTM_INDEX_FILTERS`,
);
}

/**
* Sets up the environment and validates key components for running the CDS extractor.
* This includes checking for the CodeQL executable, validating the source root directory,
Expand Down
147 changes: 147 additions & 0 deletions extractors/cds/tools/src/paths-ignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';

import jsYaml from 'js-yaml';
import { minimatch } from 'minimatch';

import { cdsExtractorLog } from './logging';

/**
* Well-known paths where a CodeQL configuration file may be located,
* relative to the source root directory. Checked in order of priority.
*/
const CODEQL_CONFIG_PATHS = [
'.github/codeql/codeql-config.yml',
'.github/codeql/codeql-config.yaml',
];

/**
* Cache for parsed paths-ignore patterns, keyed by source root.
* Avoids re-reading and re-parsing the config file on every call.
*/
const patternsCache = new Map<string, string[]>();

/**
* Shape of the subset of a CodeQL configuration file that we care about.
*/
interface CodeqlConfig {
'paths-ignore'?: string[];
}

/**
* Finds the CodeQL configuration file in the source root directory by
* checking the well-known paths in order.
*
* @param sourceRoot - The source root directory
* @returns The absolute path to the config file, or undefined if not found
*/
export function findCodeqlConfigFile(sourceRoot: string): string | undefined {
for (const configPath of CODEQL_CONFIG_PATHS) {
const fullPath = join(sourceRoot, configPath);
if (existsSync(fullPath)) {
return fullPath;
}
}
return undefined;
}

/**
* Reads the CodeQL configuration file and extracts the `paths-ignore`
* patterns list.
*
* @param sourceRoot - The source root directory
* @returns Array of paths-ignore glob patterns, or empty array if none
*/
export function getPathsIgnorePatterns(sourceRoot: string): string[] {
const cached = patternsCache.get(sourceRoot);
if (cached !== undefined) {
return cached;
}

const configPath = findCodeqlConfigFile(sourceRoot);
if (!configPath) {
patternsCache.set(sourceRoot, []);
return [];
}

try {
const content = readFileSync(configPath, 'utf8');
const config = jsYaml.load(content) as CodeqlConfig | null;

Check warning on line 69 in extractors/cds/tools/src/paths-ignore.ts

View workflow job for this annotation

GitHub Actions / CDS extractor bundle validation

Caution: `jsYaml` also has a named export `load`. Check if you meant to write `import {load} from 'js-yaml'` instead

if (!config || !Array.isArray(config['paths-ignore'])) {
Comment thread
data-douser marked this conversation as resolved.
return [];
}

const patterns = config['paths-ignore'].filter(
(p): p is string => typeof p === 'string' && p.length > 0,
);

if (patterns.length > 0) {
cdsExtractorLog(
'info',
`Found ${patterns.length} paths-ignore pattern(s) in ${configPath}: ${patterns.join(', ')}`,
);
}

patternsCache.set(sourceRoot, patterns);
return patterns;
} catch (error) {
cdsExtractorLog('warn', `Failed to read CodeQL config file at ${configPath}: ${String(error)}`);
patternsCache.set(sourceRoot, []);
return [];
}
}

/**
* Tests whether a single relative file path matches any of the given
* paths-ignore patterns.
*
* Pattern matching follows the CodeQL `paths-ignore` semantics:
* - A bare directory name `vendor` matches anything under `vendor/`
* - `**` matches across directory boundaries
* - `*` matches within a single path segment
*
* @param relativePath - File path relative to the source root
* @param patterns - Array of paths-ignore glob patterns
* @returns true if the path should be ignored
*/
export function shouldIgnorePath(relativePath: string, patterns: string[]): boolean {
for (const raw of patterns) {
// Strip trailing slashes so `vendor/` is treated the same as `vendor`
const pattern = raw.replace(/\/+$/, '');

// Direct minimatch check
if (minimatch(relativePath, pattern, { dot: true })) {
return true;
}

// Also match as a directory prefix: pattern `vendor` should
// match `vendor/lib/foo.cds` (i.e. anything nested underneath).
if (minimatch(relativePath, `${pattern}/**`, { dot: true })) {
return true;
Comment thread
data-douser marked this conversation as resolved.
}
}
return false;
}

/**
* Filters a list of relative file paths, removing any that match the
* given paths-ignore patterns.
*
* @param relativePaths - File paths relative to the source root
* @param patterns - Array of paths-ignore glob patterns
* @returns Filtered list of paths that do NOT match any ignore pattern
*/
export function filterIgnoredPaths(relativePaths: string[], patterns: string[]): string[] {
if (patterns.length === 0) {
return relativePaths;
}
return relativePaths.filter(p => !shouldIgnorePath(p, patterns));
}

/**
* Clears the internal patterns cache. Intended for testing only.
*/
export function clearPathsIgnoreCache(): void {
patternsCache.clear();
}
3 changes: 2 additions & 1 deletion extractors/cds/tools/test/src/codeql.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ jest.mock('child_process', () => ({
}));

jest.mock('../../src/environment', () => ({
getPlatformInfo: jest.fn(),
applyPathsIgnoreToLgtmFilters: jest.fn(),
configureLgtmIndexFilters: jest.fn(),
getPlatformInfo: jest.fn(),
}));

jest.mock('../../src/diagnostics', () => ({
Expand Down
41 changes: 41 additions & 0 deletions extractors/cds/tools/test/src/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ import {
setupJavaScriptExtractorEnv,
getAutobuildScriptPath,
configureLgtmIndexFilters,
applyPathsIgnoreToLgtmFilters,
setupAndValidateEnvironment,
} from '../../src/environment';
import * as pathsIgnore from '../../src/paths-ignore';

jest.mock('../../src/paths-ignore', () => ({
getPathsIgnorePatterns: jest.fn().mockReturnValue([]),
}));

// Mock modules
jest.mock('child_process');
Expand Down Expand Up @@ -410,6 +416,41 @@ describe('environment', () => {
});
});

describe('applyPathsIgnoreToLgtmFilters', () => {
beforeEach(() => {
(pathsIgnore.getPathsIgnorePatterns as jest.Mock).mockReturnValue([]);
});

it('should append exclude lines to LGTM_INDEX_FILTERS when patterns exist', () => {
(pathsIgnore.getPathsIgnorePatterns as jest.Mock).mockReturnValue(['vendor', '**/*.test.js']);
process.env.LGTM_INDEX_FILTERS = 'include:**/*.cds.json';

applyPathsIgnoreToLgtmFilters('/source');

expect(process.env.LGTM_INDEX_FILTERS).toContain('exclude:vendor');
expect(process.env.LGTM_INDEX_FILTERS).toContain('exclude:**/*.test.js');
expect(process.env.LGTM_INDEX_FILTERS).toContain('include:**/*.cds.json');
});

it('should not modify LGTM_INDEX_FILTERS when no patterns exist', () => {
(pathsIgnore.getPathsIgnorePatterns as jest.Mock).mockReturnValue([]);
process.env.LGTM_INDEX_FILTERS = 'include:**/*.cds.json';

applyPathsIgnoreToLgtmFilters('/source');

expect(process.env.LGTM_INDEX_FILTERS).toBe('include:**/*.cds.json');
});

it('should handle missing LGTM_INDEX_FILTERS env var', () => {
(pathsIgnore.getPathsIgnorePatterns as jest.Mock).mockReturnValue(['vendor']);
delete process.env.LGTM_INDEX_FILTERS;

applyPathsIgnoreToLgtmFilters('/source');

expect(process.env.LGTM_INDEX_FILTERS).toContain('exclude:vendor');
});
});

describe('setupAndValidateEnvironment', () => {
let filesystem: any;

Expand Down
Loading
Loading