Skip to content

Commit 5f65f09

Browse files
committed
Migrate external to extensible predicates w/ dataExtensions
Replace `external string` declarations with `extensible predicate` syntax across all 8 languages, enabling YAML-based data extensions for testing instead of fragile fallback hacks. Architecture: - Add ExternalPredicates.qll per language with shared extensible predicate declarations (sourceFunction, targetFunction, selectedSourceFiles) - Add dataExtensions YAML files in test/*/ext/ directories providing test values for each query's external predicates - Add dataExtensions glob to all test pack codeql-pack.yml files - Remove all "Fallback for unit tests" or clauses from queries This means: - Tests now exercise the actual external predicate logic path - No more path-based hacks (getParentContainer().getBaseName() = "test") - CallGraphFromTo tests produce precise source→target results - New queries only need a .model.yml file, not query modifications
1 parent 46c9825 commit 5f65f09

File tree

111 files changed

+756
-770
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

111 files changed

+756
-770
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ extensions/vscode/test/fixtures/
1717
node_modules
1818
query-results*
1919
server/dist/
20+
server/ql/*/tools/test/*
2021
workshops/
2122

server/dist/codeql-development-mcp-server.js

Lines changed: 62 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -57226,65 +57226,72 @@ function registerCLITool(server, definition) {
5722657226
} else if (query) {
5722757227
positionalArgs = [...positionalArgs, query];
5722857228
}
57229-
if (queryName === "PrintAST" && sourceFiles) {
57229+
const extensiblePredicates = {};
57230+
if ((queryName === "PrintAST" || queryName === "PrintCFG") && sourceFiles) {
5723057231
const filePaths = sourceFiles.split(",").map((f) => f.trim());
57231-
let tempDir;
57232-
let csvPath;
57233-
try {
57234-
tempDir = createProjectTempDir("codeql-external-");
57235-
tempDirsToCleanup.push(tempDir);
57236-
csvPath = join6(tempDir, "selectedSourceFiles.csv");
57237-
const csvContent = filePaths.join("\n") + "\n";
57238-
writeFileSync2(csvPath, csvContent, "utf8");
57239-
} catch (err) {
57240-
logger.error(`Failed to create external predicate CSV for PrintAST query at path ${csvPath || "[unknown]"}: ${err instanceof Error ? err.message : String(err)}`);
57241-
throw err;
57242-
}
57243-
const currentExternal = options.external || [];
57244-
const externalArray = Array.isArray(currentExternal) ? currentExternal : [currentExternal];
57245-
externalArray.push(`selectedSourceFiles=${csvPath}`);
57246-
options.external = externalArray;
57247-
logger.info(`Created external predicate CSV at ${csvPath} for files: ${filePaths.join(", ")}`);
57232+
extensiblePredicates["selectedSourceFiles"] = filePaths;
5724857233
}
57249-
if (queryName === "CallGraphFrom" && sourceFunction) {
57234+
if (sourceFunction) {
5725057235
const functionNames = sourceFunction.split(",").map((f) => f.trim());
57251-
let tempDir;
57252-
let csvPath;
57253-
try {
57254-
tempDir = createProjectTempDir("codeql-external-");
57255-
tempDirsToCleanup.push(tempDir);
57256-
csvPath = join6(tempDir, "sourceFunction.csv");
57257-
const csvContent = functionNames.join("\n") + "\n";
57258-
writeFileSync2(csvPath, csvContent, "utf8");
57259-
} catch (err) {
57260-
logger.error(`Failed to create external predicate CSV for CallGraphFrom query at path ${csvPath || "[unknown]"}: ${err instanceof Error ? err.message : String(err)}`);
57261-
throw err;
57262-
}
57263-
const currentExternal = options.external || [];
57264-
const externalArray = Array.isArray(currentExternal) ? currentExternal : [currentExternal];
57265-
externalArray.push(`sourceFunction=${csvPath}`);
57266-
options.external = externalArray;
57267-
logger.info(`Created external predicate CSV at ${csvPath} for functions: ${functionNames.join(", ")}`);
57236+
extensiblePredicates["sourceFunction"] = functionNames;
5726857237
}
57269-
if (queryName === "CallGraphTo" && targetFunction) {
57238+
if (targetFunction) {
5727057239
const functionNames = targetFunction.split(",").map((f) => f.trim());
57271-
let tempDir;
57272-
let csvPath;
57273-
try {
57274-
tempDir = createProjectTempDir("codeql-external-");
57275-
tempDirsToCleanup.push(tempDir);
57276-
csvPath = join6(tempDir, "targetFunction.csv");
57277-
const csvContent = functionNames.join("\n") + "\n";
57278-
writeFileSync2(csvPath, csvContent, "utf8");
57279-
} catch (err) {
57280-
logger.error(`Failed to create external predicate CSV for CallGraphTo query at path ${csvPath || "[unknown]"}: ${err instanceof Error ? err.message : String(err)}`);
57281-
throw err;
57240+
extensiblePredicates["targetFunction"] = functionNames;
57241+
}
57242+
if (Object.keys(extensiblePredicates).length > 0) {
57243+
let targetPackName;
57244+
if (_queryLanguage) {
57245+
targetPackName = `advanced-security/ql-mcp-${_queryLanguage}-tools-src`;
57246+
} else if (query && typeof query === "string") {
57247+
const match = query.match(/\/ql\/([^/]+)\/tools\/src\//);
57248+
if (match) {
57249+
targetPackName = `advanced-security/ql-mcp-${match[1]}-tools-src`;
57250+
}
57251+
}
57252+
if (targetPackName) {
57253+
try {
57254+
const extPackDir = createProjectTempDir("codeql-ext-pack-");
57255+
tempDirsToCleanup.push(extPackDir);
57256+
const qlpackContent = [
57257+
"library: true",
57258+
"name: advanced-security/ql-mcp-runtime-extensions",
57259+
"version: 0.0.0",
57260+
"extensionTargets:",
57261+
` ${targetPackName}: "*"`,
57262+
"dataExtensions:",
57263+
' - "ext/*.model.yml"',
57264+
""
57265+
].join("\n");
57266+
writeFileSync2(join6(extPackDir, "qlpack.yml"), qlpackContent, "utf8");
57267+
const extDir = join6(extPackDir, "ext");
57268+
mkdirSync5(extDir, { recursive: true });
57269+
const extensions = ["extensions:"];
57270+
for (const [predName, values] of Object.entries(extensiblePredicates)) {
57271+
extensions.push(" - addsTo:");
57272+
extensions.push(` pack: ${targetPackName}`);
57273+
extensions.push(` extensible: ${predName}`);
57274+
extensions.push(" data:");
57275+
for (const val of values) {
57276+
extensions.push(` - ["${val}"]`);
57277+
}
57278+
}
57279+
extensions.push("");
57280+
writeFileSync2(join6(extDir, "runtime.model.yml"), extensions.join("\n"), "utf8");
57281+
const existingPacks = options["additional-packs"];
57282+
options["additional-packs"] = existingPacks ? `${existingPacks}:${extPackDir}` : extPackDir;
57283+
const modelPacks = options["model-packs"];
57284+
const modelPacksArray = Array.isArray(modelPacks) ? modelPacks : [];
57285+
modelPacksArray.push("advanced-security/ql-mcp-runtime-extensions@*");
57286+
options["model-packs"] = modelPacksArray;
57287+
logger.info(`Created runtime extension pack at ${extPackDir} targeting ${targetPackName} with predicates: ${Object.keys(extensiblePredicates).join(", ")}`);
57288+
} catch (err) {
57289+
logger.error(`Failed to create runtime extension pack: ${err instanceof Error ? err.message : String(err)}`);
57290+
throw err;
57291+
}
57292+
} else {
57293+
logger.warn("Could not determine target pack name for extensible predicates \u2014 queryLanguage not set and query path does not match expected pattern");
5728257294
}
57283-
const currentExternal = options.external || [];
57284-
const externalArray = Array.isArray(currentExternal) ? currentExternal : [currentExternal];
57285-
externalArray.push(`targetFunction=${csvPath}`);
57286-
options.external = externalArray;
57287-
logger.info(`Created external predicate CSV at ${csvPath} for functions: ${functionNames.join(", ")}`);
5728857295
}
5728957296
break;
5729057297
}
@@ -57348,7 +57355,8 @@ function registerCLITool(server, definition) {
5734857355
const defaultExamplesPath = resolve4(packageRootDir, "ql", "javascript", "examples");
5734957356
const additionalPacksPath = process.env.CODEQL_ADDITIONAL_PACKS || (existsSync4(defaultExamplesPath) ? defaultExamplesPath : void 0);
5735057357
if (additionalPacksPath && (name === "codeql_test_run" || name === "codeql_query_run" || name === "codeql_query_compile" || name === "codeql_database_analyze")) {
57351-
options["additional-packs"] = additionalPacksPath;
57358+
const existingAdditionalPacks = options["additional-packs"];
57359+
options["additional-packs"] = existingAdditionalPacks ? `${existingAdditionalPacks}:${additionalPacksPath}` : additionalPacksPath;
5735257360
}
5735357361
if (name === "codeql_test_run") {
5735457362
options["keep-databases"] = true;

server/dist/codeql-development-mcp-server.js.map

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/ql/cpp/tools/src/CallGraphFrom/CallGraphFrom.ql

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@
88
*/
99

1010
import cpp
11-
12-
/**
13-
* Gets the source function name for which to generate the call graph.
14-
* Can be a single function name or comma-separated list of function names.
15-
*/
16-
external string sourceFunction();
11+
import ExternalPredicates
1712

1813
/**
1914
* Gets a single source function name from the comma-separated list.
2015
*/
21-
string getSourceFunctionName() { result = sourceFunction().splitAt(",").trim() }
16+
string getSourceFunctionName() {
17+
exists(string s | sourceFunction(s) | result = s.splitAt(",").trim())
18+
}
2219

2320
/**
2421
* Gets a function by matching against the selected source function names.
@@ -40,12 +37,5 @@ from FunctionCall call, Function source, Function callee
4037
where
4138
call.getTarget() = callee and
4239
call.getEnclosingFunction() = source and
43-
(
44-
// Use external predicate if available
45-
source = getSourceFunction()
46-
or
47-
// Fallback for unit tests: include test files
48-
not exists(getSourceFunction()) and
49-
source.getFile().getParentContainer().getParentContainer().getBaseName() = "test"
50-
)
40+
source = getSourceFunction()
5141
select call, "Call from `" + source.getQualifiedName() + "` to `" + callee.getQualifiedName() + "`"

server/ql/cpp/tools/src/CallGraphFromTo/CallGraphFromTo.ql

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,21 @@
88
*/
99

1010
import cpp
11-
12-
/**
13-
* Gets the source function name for call graph reachability analysis.
14-
* Can be a single function name or comma-separated list of function names.
15-
*/
16-
external string sourceFunction();
17-
18-
/**
19-
* Gets the target function name for call graph reachability analysis.
20-
* Can be a single function name or comma-separated list of function names.
21-
*/
22-
external string targetFunction();
11+
import ExternalPredicates
2312

2413
/**
2514
* Gets a single source function name from the comma-separated list.
2615
*/
27-
string getSourceFunctionName() { result = sourceFunction().splitAt(",").trim() }
16+
string getSourceFunctionName() {
17+
exists(string s | sourceFunction(s) | result = s.splitAt(",").trim())
18+
}
2819

2920
/**
3021
* Gets a single target function name from the comma-separated list.
3122
*/
32-
string getTargetFunctionName() { result = targetFunction().splitAt(",").trim() }
23+
string getTargetFunctionName() {
24+
exists(string s | targetFunction(s) | result = s.splitAt(",").trim())
25+
}
3326

3427
/**
3528
* Gets a function by matching against the selected source function names.
@@ -74,19 +67,11 @@ from FunctionCall call, Function caller, Function callee
7467
where
7568
call.getTarget() = callee and
7669
call.getEnclosingFunction() = caller and
77-
(
78-
// Use external predicates if available: show calls on paths from source to target
79-
exists(Function source, Function target |
80-
source = getSourceFunction() and
81-
target = getTargetFunction() and
82-
calls*(source, caller) and
83-
calls*(callee, target)
84-
)
85-
or
86-
// Fallback for unit tests: include test files
87-
not exists(getSourceFunctionName()) and
88-
not exists(getTargetFunctionName()) and
89-
caller.getFile().getParentContainer().getParentContainer().getBaseName() = "test"
70+
exists(Function source, Function target |
71+
source = getSourceFunction() and
72+
target = getTargetFunction() and
73+
calls*(source, caller) and
74+
calls*(callee, target)
9075
)
9176
select call,
9277
"Reachable call from `" + caller.getQualifiedName() + "` to `" + callee.getQualifiedName() + "`"

server/ql/cpp/tools/src/CallGraphTo/CallGraphTo.ql

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@
88
*/
99

1010
import cpp
11-
12-
/**
13-
* Gets the target function name for which to generate the call graph.
14-
* Can be a single function name or comma-separated list of function names.
15-
*/
16-
external string targetFunction();
11+
import ExternalPredicates
1712

1813
/**
1914
* Gets a single target function name from the comma-separated list.
2015
*/
21-
string getTargetFunctionName() { result = targetFunction().splitAt(",").trim() }
16+
string getTargetFunctionName() {
17+
exists(string s | targetFunction(s) | result = s.splitAt(",").trim())
18+
}
2219

2320
/**
2421
* Gets a function by matching against the selected target function names.
@@ -40,12 +37,5 @@ from FunctionCall call, Function target, Function caller
4037
where
4138
call.getTarget() = target and
4239
call.getEnclosingFunction() = caller and
43-
(
44-
// Use external predicate if available
45-
target = getTargetFunction()
46-
or
47-
// Fallback for unit tests: include test files
48-
not exists(getTargetFunction()) and
49-
target.getFile().getParentContainer().getParentContainer().getBaseName() = "test"
50-
)
40+
target = getTargetFunction()
5141
select call, "Call to `" + target.getQualifiedName() + "` from `" + caller.getQualifiedName() + "`"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/**
2+
* Shared extensible predicate declarations for MCP server tools queries.
3+
* Values are provided via dataExtensions YAML files during testing,
4+
* or via CSV files at runtime from the MCP server.
5+
*/
6+
7+
/** Holds for each source function name for call graph analysis. */
8+
extensible predicate sourceFunction(string name);
9+
10+
/** Holds for each target function name for call graph analysis. */
11+
extensible predicate targetFunction(string name);
12+
13+
/** Holds for each selected source file path for AST/CFG printing. */
14+
extensible predicate selectedSourceFiles(string path);

server/ql/cpp/tools/src/PrintAST/PrintAST.ql

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,14 @@
88

99
import cpp
1010
import semmle.code.cpp.PrintAST
11-
12-
/**
13-
* Gets the source files to generate AST from.
14-
* Can be a single file path or comma-separated list of file paths.
15-
*/
16-
external string selectedSourceFiles();
11+
import ExternalPredicates
1712

1813
/**
1914
* Gets a single source file from the comma-separated list.
2015
*/
21-
string getSelectedSourceFile() { result = selectedSourceFiles().splitAt(",").trim() }
16+
string getSelectedSourceFile() {
17+
exists(string s | selectedSourceFiles(s) | result = s.splitAt(",").trim())
18+
}
2219

2320
/**
2421
* Gets a file by matching against the selected source file paths.
@@ -45,12 +42,5 @@ File getSelectedFile() {
4542
* Falls back to test directory structure when external predicates are not available.
4643
*/
4744
class Cfg extends PrintAstConfiguration {
48-
override predicate shouldPrintDeclaration(Declaration decl) {
49-
// Use external predicate if available
50-
decl.getFile() = getSelectedFile()
51-
or
52-
// Fallback for unit tests: include test files
53-
not exists(getSelectedFile()) and
54-
decl.getFile().getParentContainer().getParentContainer().getBaseName() = "test"
55-
}
45+
override predicate shouldPrintDeclaration(Declaration decl) { decl.getFile() = getSelectedFile() }
5646
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
| Example1.cpp:7:5:7:14 | call to unrelated1 | Call from `unrelated2` to `unrelated1` |
21
| Example1.cpp:12:5:12:14 | call to unrelated1 | Call from `sourceFunc` to `unrelated1` |
32
| Example1.cpp:13:5:13:14 | call to unrelated2 | Call from `sourceFunc` to `unrelated2` |
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
1-
| Example1.cpp:6:5:6:13 | call to unrelated | Reachable call from `target` to `unrelated` |
21
| Example1.cpp:10:5:10:10 | call to target | Reachable call from `intermediate` to `target` |
32
| Example1.cpp:14:5:14:16 | call to intermediate | Reachable call from `source` to `intermediate` |

0 commit comments

Comments
 (0)