Files
codebase-memory-mcp/internal/cbm/spawn_patterns.c
Martin Vogel 42a9320200 graph: one identity per conditional definition, SPAWNS edges, no cross-language guesses
The test-impact engine selects tests by walking the graph, so it can only
be as right as the graph. Three gaps made it wrong:

One identity for a definition's mutually exclusive variants
- A definition written once per #if/#else branch, per #[cfg], per
  platform file (foo_linux.c / foo_win.c, platform directories) or per Go
  build tag is ONE node. The graph buffer keeps every variant's span in a
  `variants` node property (definition labels only), so a change in any
  variant maps to the definition.
- Extraction now sees definitions inside conditional blocks where it
  skipped them: C++/C#/ObjC class members under #if (one cursor walk,
  conditional block types per language in lang_specs), Elixir defs under
  if/unless, Fortran .F90/.F95/.F03/.F08, Rust #[cfg] twins (no more
  `name#cfg(...)` QNs that lost every call), C platform files and
  directories (non-static file-scope functions).
- Haskell: the vendored scanner reads only an #if group's first branch.
  The grammar stays upstream; every further branch is parsed as its own
  same-length view (cpp_branch_views.c) and adds what the raw parse
  missed. haskell/bytestring: no new partial parses, +27 functions,
  IMPORTS 322 -> 329, 131 definitions with variant spans.
- Incremental closure repair re-parses every file of such a definition
  when one of them changes (variant partners through DEFINES), so a
  repair equals a full index.

No cross-language name guesses
- A name-only resolution (unique_name, suffix_match, qualified_suffix,
  field_type_hint, and same_module across files that only share a path
  stem) never binds a symbol another language defines.
- Bash imports are the files a script sources: every top-level command
  was read as an import of its name (`set -e` imported "set", bound to a
  TSX method in 137 scripts here).
- This repository: cross-language CALLS 5,916 -> 5,091; the remainder are
  C/C++ pairs, builtins and same-stem module collisions (recorded).

SPAWNS edges (per language, like the HTTP client patterns)
- A call that starts a program (subprocess.run, exec.Command,
  child_process.spawn, posix_spawn, system, Process.Start, Command::new,
  ... about 30 languages, through the file's own imports) gets a SPAWNS
  edge to a Process node named after the program (literal first word, or
  <dynamic>) instead of a CALLS guess. No target resolution. Both
  resolvers ask at the same point (parity test).

The engine follows the graph
- Seeds map changed lines through every variant span; a test written
  once per #if branch is one test; when a change reaches a non-test
  `main`, the tests that spawn processes are reached too (the process
  runs main, and main reaches everything).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-10-06 02:44:56 +02:00

745 lines
26 KiB
C

/*
* spawn_patterns.c — process-spawning call sites, per language (see the header).
*
* Spellings come from what the extractor records as the callee (checked per
* language against real calls): Rust `Command::new`, Go `exec.Command`, Java
* `Runtime.getRuntime().exec`, a constructor as its class name.
*/
#include "spawn_patterns.h"
#include "foundation/constants.h"
#include <stdio.h>
#include <string.h>
#define Q CBM_SPAWN_QUALIFIED
#define U CBM_SPAWN_BARE
#define I CBM_SPAWN_BARE_IMPORTED
#define S CBM_SPAWN_STRING_RECEIVER
/* libc / POSIX / Win32. posix_spawn and _spawn* take the path second;
* CreateProcess names the program first or only in its command line. */
static const cbm_spawn_api_t c_family[] = {
{"system", NULL, U, 0, -1}, {"std::system", NULL, U, 0, -1},
{"_wsystem", NULL, U, 0, -1}, {"popen", NULL, U, 0, -1},
{"_popen", NULL, U, 0, -1}, {"_wpopen", NULL, U, 0, -1},
{"execl", NULL, U, 0, -1}, {"execle", NULL, U, 0, -1},
{"execlp", NULL, U, 0, -1}, {"execv", NULL, U, 0, -1},
{"execve", NULL, U, 0, -1}, {"execvp", NULL, U, 0, -1},
{"execvpe", NULL, U, 0, -1}, {"fexecve", NULL, U, 0, -1},
{"posix_spawn", NULL, U, 1, -1}, {"posix_spawnp", NULL, U, 1, -1},
{"_execl", NULL, U, 0, -1}, {"_execv", NULL, U, 0, -1},
{"_execlp", NULL, U, 0, -1}, {"_execvp", NULL, U, 0, -1},
{"_spawnl", NULL, U, 1, -1}, {"_spawnle", NULL, U, 1, -1},
{"_spawnlp", NULL, U, 1, -1}, {"_spawnv", NULL, U, 1, -1},
{"_spawnve", NULL, U, 1, -1}, {"_spawnvp", NULL, U, 1, -1},
{"_wspawnv", NULL, U, 1, -1}, {"_wspawnvp", NULL, U, 1, -1},
{"CreateProcess", NULL, U, 0, 1}, {"CreateProcessA", NULL, U, 0, 1},
{"CreateProcessW", NULL, U, 0, 1}, {"ShellExecute", NULL, U, 2, -1},
{"ShellExecuteA", NULL, U, 2, -1}, {"ShellExecuteW", NULL, U, 2, -1},
{"ShellExecuteExA", NULL, U, 0, -1}, {"ShellExecuteExW", NULL, U, 0, -1},
{"WinExec", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t python[] = {
{"subprocess.run", NULL, Q, 0, -1},
{"subprocess.call", NULL, Q, 0, -1},
{"subprocess.check_call", NULL, Q, 0, -1},
{"subprocess.check_output", NULL, Q, 0, -1},
{"subprocess.Popen", NULL, Q, 0, -1},
{"subprocess.getoutput", NULL, Q, 0, -1},
{"subprocess.getstatusoutput", NULL, Q, 0, -1},
{"os.system", NULL, Q, 0, -1},
{"os.popen", NULL, Q, 0, -1},
{"os.startfile", NULL, Q, 0, -1},
{"os.execl", NULL, Q, 0, -1},
{"os.execle", NULL, Q, 0, -1},
{"os.execlp", NULL, Q, 0, -1},
{"os.execlpe", NULL, Q, 0, -1},
{"os.execv", NULL, Q, 0, -1},
{"os.execve", NULL, Q, 0, -1},
{"os.execvp", NULL, Q, 0, -1},
{"os.execvpe", NULL, Q, 0, -1},
{"os.posix_spawn", NULL, Q, 0, -1},
{"os.posix_spawnp", NULL, Q, 0, -1},
{"os.spawnl", NULL, Q, 1, -1},
{"os.spawnle", NULL, Q, 1, -1},
{"os.spawnlp", NULL, Q, 1, -1},
{"os.spawnlpe", NULL, Q, 1, -1},
{"os.spawnv", NULL, Q, 1, -1},
{"os.spawnve", NULL, Q, 1, -1},
{"os.spawnvp", NULL, Q, 1, -1},
{"os.spawnvpe", NULL, Q, 1, -1},
{"asyncio.create_subprocess_exec", NULL, Q, 0, -1},
{"asyncio.create_subprocess_shell", NULL, Q, 0, -1},
{"pexpect.spawn", NULL, Q, 0, -1},
{"pexpect.run", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* Node's child_process (also behind `node:`), the destructured-require form,
* execa / cross-spawn, and the Bun and Deno globals. */
static const cbm_spawn_api_t js_family[] = {
{"child_process.spawn", NULL, Q, 0, -1},
{"child_process.spawnSync", NULL, Q, 0, -1},
{"child_process.exec", NULL, Q, 0, -1},
{"child_process.execSync", NULL, Q, 0, -1},
{"child_process.execFile", NULL, Q, 0, -1},
{"child_process.execFileSync", NULL, Q, 0, -1},
{"child_process.fork", NULL, Q, 0, -1},
{"spawn", "child_process", I, 0, -1},
{"spawnSync", "child_process", I, 0, -1},
{"exec", "child_process", I, 0, -1},
{"execSync", "child_process", I, 0, -1},
{"execFile", "child_process", I, 0, -1},
{"execFileSync", "child_process", I, 0, -1},
{"fork", "child_process", I, 0, -1},
{"execa.execa", NULL, Q, 0, -1},
{"execa.execaSync", NULL, Q, 0, -1},
{"execa.execaCommand", NULL, Q, 0, -1},
{"execa.execaCommandSync", NULL, Q, 0, -1},
{"cross-spawn.spawn", NULL, Q, 0, -1},
{"cross-spawn.sync", NULL, Q, 0, -1},
{"Bun.spawn", NULL, Q, 0, -1},
{"Bun.spawnSync", NULL, Q, 0, -1},
{"Deno.Command", NULL, Q, 0, -1},
{"Deno.run", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t go[] = {
{"os/exec.Command", NULL, Q, 0, -1},
{"os/exec.CommandContext", NULL, Q, 1, -1},
{"golang.org/x/sys/execabs.Command", NULL, Q, 0, -1},
{"golang.org/x/sys/execabs.CommandContext", NULL, Q, 1, -1},
{"os.StartProcess", NULL, Q, 0, -1},
{"syscall.Exec", NULL, Q, 0, -1},
{"syscall.ForkExec", NULL, Q, 0, -1},
{"syscall.StartProcess", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t rust[] = {
{"std::process::Command::new", NULL, Q, 0, -1},
{"tokio::process::Command::new", NULL, Q, 0, -1},
{"async_process::Command::new", NULL, Q, 0, -1},
{"async_std::process::Command::new", NULL, Q, 0, -1},
{"smol::process::Command::new", NULL, Q, 0, -1},
{"duct::cmd", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* java.lang is implicit, so the class name alone is the spelling. */
static const cbm_spawn_api_t jvm[] = {
{"ProcessBuilder", NULL, U, 0, -1},
{"java.lang.ProcessBuilder", NULL, Q, 0, -1},
{"Runtime.getRuntime().exec", NULL, U, 0, -1},
{".execute", NULL, S, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t dotnet[] = {
{"Process.Start", NULL, U, 0, -1}, {"System.Diagnostics.Process.Start", NULL, Q, 0, -1},
{"ProcessStartInfo", NULL, U, 0, -1}, {"System.Diagnostics.ProcessStartInfo", NULL, Q, 0, -1},
{"Cli.Wrap", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t ruby[] = {
{"system", NULL, U, 0, -1}, {"exec", NULL, U, 0, -1},
{"spawn", NULL, U, 0, -1}, {"Kernel.system", NULL, U, 0, -1},
{"Kernel.exec", NULL, U, 0, -1}, {"Kernel.spawn", NULL, U, 0, -1},
{"Process.spawn", NULL, U, 0, -1}, {"Process.exec", NULL, U, 0, -1},
{"IO.popen", NULL, U, 0, -1}, {"PTY.spawn", NULL, U, 0, -1},
{"Open3.popen2", NULL, U, 0, -1}, {"Open3.popen2e", NULL, U, 0, -1},
{"Open3.popen3", NULL, U, 0, -1}, {"Open3.capture2", NULL, U, 0, -1},
{"Open3.capture2e", NULL, U, 0, -1}, {"Open3.capture3", NULL, U, 0, -1},
{"Open3.pipeline", NULL, U, 0, -1}, {NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t php[] = {
{"exec", NULL, U, 0, -1},
{"shell_exec", NULL, U, 0, -1},
{"system", NULL, U, 0, -1},
{"passthru", NULL, U, 0, -1},
{"proc_open", NULL, U, 0, -1},
{"popen", NULL, U, 0, -1},
{"pcntl_exec", NULL, U, 0, -1},
{"Symfony\\Component\\Process\\Process", NULL, Q, 0, -1},
{"Symfony\\Component\\Process\\Process::fromShellCommandline", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t perl[] = {
{"system", NULL, U, 0, -1},
{"exec", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t lua[] = {
{"os.execute", NULL, U, 0, -1},
{"io.popen", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t r_lang[] = {
{"system", NULL, U, 0, -1},
{"system2", NULL, U, 0, -1},
{"shell", NULL, U, 0, -1},
{"processx::run", NULL, Q, 0, -1},
{"sys::exec_wait", NULL, Q, 0, -1},
{"sys::exec_internal", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t elixir[] = {
{"System.cmd", NULL, U, 0, -1},
{"System.shell", NULL, U, 0, -1},
{"Port.open", NULL, U, 0, CBM_SPAWN_ANY_LITERAL},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t erlang[] = {
{"os:cmd", NULL, Q, 0, -1},
{"open_port", NULL, U, 0, -1},
{"erlang:open_port", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t haskell[] = {
{"callProcess", "System.Process", I, 0, -1},
{"callCommand", "System.Process", I, 0, -1},
{"readProcess", "System.Process", I, 0, -1},
{"readProcessWithExitCode", "System.Process", I, 0, -1},
{"readCreateProcess", "System.Process", I, 0, -1},
{"readCreateProcessWithExitCode", "System.Process", I, 0, -1},
{"createProcess", "System.Process", I, 0, -1},
{"spawnProcess", "System.Process", I, 0, -1},
{"spawnCommand", "System.Process", I, 0, -1},
{"system", "System.Process", I, 0, -1},
{"rawSystem", "System.Process", I, 0, -1},
{"runProcess", "System.Process", I, 0, -1},
{"runCommand", "System.Process", I, 0, -1},
{"runInteractiveProcess", "System.Process", I, 0, -1},
{"runInteractiveCommand", "System.Process", I, 0, -1},
{"runProcess", "System.Process.Typed", I, 0, -1},
{"readProcess", "System.Process.Typed", I, 0, -1},
{"startProcess", "System.Process.Typed", I, 0, -1},
{"system", "System.Cmd", I, 0, -1},
{"rawSystem", "System.Cmd", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t dlang[] = {
{"execute", "std.process", I, 0, -1},
{"executeShell", "std.process", I, 0, -1},
{"spawnProcess", "std.process", I, 0, -1},
{"spawnShell", "std.process", I, 0, -1},
{"pipeProcess", "std.process", I, 0, -1},
{"pipeShell", "std.process", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
/* Foundation's Process: the program is a property set after construction,
* so a bare construction is a spawn site with a dynamic program. */
static const cbm_spawn_api_t swift[] = {
{"Process.launchedProcess", NULL, U, 0, -1},
{"Process.run", NULL, U, 0, -1},
{"Process", "Foundation", I, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t crystal[] = {
{"Process.run", NULL, U, 0, -1}, {"Process.new", NULL, U, 0, -1},
{"Process.exec", NULL, U, 0, -1}, {"system", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t zig[] = {
{"std.process.Child.init", NULL, Q, 0, -1},
{"std.process.Child.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.ChildProcess.init", NULL, Q, 0, -1},
{"std.ChildProcess.run", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.ChildProcess.exec", NULL, Q, 0, CBM_SPAWN_ANY_LITERAL},
{"std.process.execv", NULL, Q, 1, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t ocaml[] = {
{"Sys.command", NULL, U, 0, -1},
{"Unix.system", NULL, U, 0, -1},
{"Unix.create_process", NULL, U, 0, -1},
{"Unix.create_process_env", NULL, U, 0, -1},
{"Unix.open_process", NULL, U, 0, -1},
{"Unix.open_process_in", NULL, U, 0, -1},
{"Unix.open_process_out", NULL, U, 0, -1},
{"Unix.open_process_full", NULL, U, 0, -1},
{"Unix.open_process_args", NULL, U, 0, -1},
{"Unix.execv", NULL, U, 0, -1},
{"Unix.execvp", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t clojure[] = {
{"clojure.java.shell/sh", NULL, Q, 0, -1},
{"sh", "clojure.java.shell", I, 0, -1},
{"babashka.process/process", NULL, Q, 0, -1},
{"babashka.process/shell", NULL, Q, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t tcl[] = {
{"exec", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t fortran[] = {
{"execute_command_line", NULL, U, 0, -1},
{"system", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t julia[] = {
{"run", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
static const cbm_spawn_api_t powershell[] = {
{"Start-Process", NULL, U, 0, -1},
{NULL, NULL, 0, 0, -1},
};
#undef Q
#undef U
#undef I
#undef S
static const cbm_spawn_api_t *spawn_table(CBMLanguage lang) {
switch (lang) {
case CBM_LANG_C:
case CBM_LANG_CPP:
case CBM_LANG_CUDA:
case CBM_LANG_OBJC:
return c_family;
case CBM_LANG_PYTHON:
return python;
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
case CBM_LANG_ARKTS:
return js_family;
case CBM_LANG_GO:
return go;
case CBM_LANG_RUST:
return rust;
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
case CBM_LANG_SCALA:
case CBM_LANG_GROOVY:
return jvm;
case CBM_LANG_CSHARP:
case CBM_LANG_FSHARP:
return dotnet;
case CBM_LANG_RUBY:
return ruby;
case CBM_LANG_PHP:
return php;
case CBM_LANG_PERL:
return perl;
case CBM_LANG_LUA:
case CBM_LANG_LUAU:
case CBM_LANG_TEAL:
return lua;
case CBM_LANG_R:
return r_lang;
case CBM_LANG_ELIXIR:
return elixir;
case CBM_LANG_ERLANG:
return erlang;
case CBM_LANG_HASKELL:
return haskell;
case CBM_LANG_DLANG:
return dlang;
case CBM_LANG_SWIFT:
return swift;
case CBM_LANG_CRYSTAL:
return crystal;
case CBM_LANG_ZIG:
return zig;
case CBM_LANG_OCAML:
return ocaml;
case CBM_LANG_CLOJURE:
return clojure;
case CBM_LANG_TCL:
return tcl;
case CBM_LANG_FORTRAN:
return fortran;
case CBM_LANG_JULIA:
return julia;
case CBM_LANG_POWERSHELL:
return powershell;
default:
return NULL;
}
}
/* An import value without Node's `node:` scheme. */
static const char spawn_node_scheme[] = "node:";
static const char *spawn_module(const char *val) {
size_t n = sizeof(spawn_node_scheme) - 1;
return strncmp(val, spawn_node_scheme, n) == 0 ? val + n : val;
}
static const char *const spawn_separators[] = {"::", ".", ":", "/", "\\", NULL};
/* Length of the callee's head (before its first separator) and the
* separator's offset, or 0 when the callee has none. */
static size_t spawn_head_len(const char *callee) {
size_t best = 0;
for (int i = 0; spawn_separators[i]; i++) {
const char *at = strstr(callee, spawn_separators[i]);
if (at && at > callee && (best == 0 || (size_t)(at - callee) < best)) {
best = (size_t)(at - callee);
}
}
return best;
}
/* True when `val` names `name` as its last segment ("subprocess.run" for
* run, "java.lang.ProcessBuilder" for ProcessBuilder). */
static bool spawn_val_names(const char *val, const char *name) {
size_t vl = strlen(val);
size_t nl = strlen(name);
if (vl <= nl || strcmp(val + vl - nl, name) != 0) {
return false;
}
char sep = val[vl - nl - 1];
return sep == '.' || sep == ':' || sep == '/' || sep == '\\';
}
/* The callee as its import makes it: `sp.run` -> subprocess.run (an alias),
* `run` -> subprocess.run (a member import), `spawn` -> child_process.spawn
* (a named import mapped to its module). Empty when no import applies. */
static void spawn_expand(const char *callee, const CBMImport *imports, int count, char *out,
size_t n) {
out[0] = '\0';
size_t head = spawn_head_len(callee);
size_t key_len = head ? head : strlen(callee);
for (int i = 0; i < count; i++) {
const char *key = imports[i].local_name;
if (!key || !imports[i].module_path || strlen(key) != key_len ||
strncmp(key, callee, key_len) != 0) {
continue;
}
const char *val = spawn_module(imports[i].module_path);
if (head) {
snprintf(out, n, "%s%s", val, callee + head);
} else if (spawn_val_names(val, callee)) {
snprintf(out, n, "%s", val);
} else {
snprintf(out, n, "%s.%s", val, callee);
}
return;
}
}
static bool spawn_imports_module(const CBMImport *imports, int count, const char *module) {
size_t ml = strlen(module);
for (int i = 0; i < count; i++) {
if (!imports[i].module_path) {
continue;
}
const char *val = spawn_module(imports[i].module_path);
if (strncmp(val, module, ml) == 0 &&
(val[ml] == '\0' || val[ml] == '.' || val[ml] == ':' || val[ml] == '/')) {
return true;
}
}
return false;
}
/* Groovy `"git status".execute`: a quoted receiver, then the api suffix. */
static bool spawn_string_receiver(const char *callee, const char *suffix) {
size_t cl = strlen(callee);
size_t sl = strlen(suffix);
return (callee[0] == '"' || callee[0] == '\'') && cl > sl + 1 &&
strcmp(callee + cl - sl, suffix) == 0 && callee[cl - sl - 1] == callee[0];
}
static bool spawn_entry_matches(const cbm_spawn_api_t *e, const char *callee, const char *expanded,
const CBMImport *imports, int count) {
switch (e->rule) {
case CBM_SPAWN_QUALIFIED:
return strcmp(callee, e->api) == 0 || (expanded[0] && strcmp(expanded, e->api) == 0);
case CBM_SPAWN_BARE:
return strcmp(callee, e->api) == 0;
case CBM_SPAWN_BARE_IMPORTED:
return strcmp(callee, e->api) == 0 && spawn_imports_module(imports, count, e->module);
case CBM_SPAWN_STRING_RECEIVER:
return spawn_string_receiver(callee, e->api);
default:
return false;
}
}
const cbm_spawn_api_t *cbm_spawn_match(CBMLanguage lang, const char *callee,
const CBMImport *imports, int count) {
const cbm_spawn_api_t *table = spawn_table(lang);
if (!table || !callee || !callee[0]) {
return NULL;
}
if (!imports) {
count = 0;
}
char expanded[CBM_SZ_512];
spawn_expand(callee, imports, count, expanded, sizeof(expanded));
for (const cbm_spawn_api_t *e = table; e->api; e++) {
if (spawn_entry_matches(e, callee, expanded, imports, count)) {
return e;
}
}
return NULL;
}
/* The first quoted literal in `text` ("..", '..' or `..`), copied without
* its quotes. False when there is none. */
static bool spawn_first_literal(const char *text, char *out, size_t n) {
for (const char *p = text; *p; p++) {
if (*p != '"' && *p != '\'' && *p != '`') {
continue;
}
const char *end = p + 1;
while (*end && *end != *p) {
end += (*end == '\\' && end[1]) ? 2 : 1;
}
if (!*end) {
return false;
}
size_t len = (size_t)(end - p - 1);
if (len >= n) {
len = n - 1;
}
memcpy(out, p + 1, len);
out[len] = '\0';
return true;
}
return false;
}
static const char *spawn_arg_expr(const CBMCall *call, int index) {
for (int i = 0; i < call->arg_count; i++) {
if (call->args[i].index == index && call->args[i].expr) {
return call->args[i].expr;
}
}
return NULL;
}
static bool spawn_word_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
static const char *spawn_skip_space(const char *p) {
while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n') {
p++;
}
return p;
}
/* Past a keyword label in front of an argument (`launchPath: `, `args=`). */
static const char *spawn_skip_label(const char *p) {
const char *q = p;
while (spawn_word_char(*q)) {
q++;
}
if (q == p) {
return p;
}
q = spawn_skip_space(q);
if ((*q == ':' && q[1] != ':') || (*q == '=' && q[1] != '=')) {
return spawn_skip_space(q + 1);
}
return p;
}
/* A string literal at the start of `p`, after a prefix such as r, b, f, L
* or @ (r"..", L"..", @".."). */
static bool spawn_leading_literal(const char *p, char *out, size_t n) {
for (int i = 0; i < 2 && *p && strchr("rRbBuUfFL@$", *p); i++) {
p++;
}
return (*p == '"' || *p == '\'' || *p == '`') && spawn_first_literal(p, out, n);
}
/* What may stand before a collection's bracket: nothing, Zig's `&.` / `.`,
* Java's `new String[]`, or a collection constructor. A name before `[` is a
* subscript (`record["argv"]`) and any other call's arguments are not the
* command (`os.path.join("bin", tool)`). */
static bool spawn_collection_prefix(const char *p, size_t len) {
static const char *const ctors[] = {
"listOf", "arrayOf", "mutableListOf", "List.of", "Arrays.asList", "vec!", "c", "Cmd", NULL};
if (len == 0 || (len <= 2 && strspn(p, "&.") == len) ||
(len > 2 && strncmp(p + len - 2, "[]", 2) == 0 && strncmp(p, "new ", 4) == 0)) {
return true;
}
for (int i = 0; ctors[i]; i++) {
if (strlen(ctors[i]) == len && strncmp(p, ctors[i], len) == 0) {
return true;
}
}
return false;
}
/* The first element of a collection written at `p` ([..], (..), {..},
* listOf(..), &.{..}), copied into `out`. False when `p` is no collection. */
static bool spawn_first_element(const char *p, char *out, size_t n) {
const char *open = p;
while (*open && (spawn_word_char(*open) || strchr("!.&:<> ", *open) ||
(open[0] == '[' && open[1] == ']'))) {
open += (open[0] == '[') ? 2 : 1;
}
if ((*open != '[' && *open != '(' && *open != '{') ||
!spawn_collection_prefix(p, (size_t)(open - p))) {
return false;
}
p = open;
const char *start = spawn_skip_space(p + 1);
int depth = 0;
const char *end = start;
for (; *end; end++) {
if (*end == '"' || *end == '\'' || *end == '`') {
const char *close = strchr(end + 1, *end);
if (!close) {
break;
}
end = close;
} else if (strchr("[({", *end)) {
depth++;
} else if (strchr("])}", *end)) {
if (depth-- == 0) {
break;
}
} else if (*end == ',' && depth == 0) {
break;
}
}
size_t len = (size_t)(end - start);
if (len == 0 || len >= n) {
return false;
}
memcpy(out, start, len);
out[len] = '\0';
return true;
}
/* The program literal an argument spells: a leading literal, or the first
* element of a collection when that element is one. A program held in a
* variable (`[binary, "--version"]`) is no literal: the arguments after it
* are not the program. */
static bool spawn_expr_literal(const char *expr, char *out, size_t n) {
char element[CBM_SZ_256];
for (int depth = 0; depth < 3; depth++) {
expr = spawn_skip_label(spawn_skip_space(expr));
if (spawn_leading_literal(expr, out, n)) {
return true;
}
if (!spawn_first_element(expr, element, sizeof(element))) {
return false;
}
expr = element;
}
return false;
}
/* A literal from the argument at `index`. Tcl words are literals unquoted;
* elsewhere an unquoted argument counts only when the extractor itself read
* it as the call's string (Perl's `system("make")`). */
static bool spawn_arg_literal(const CBMCall *call, CBMLanguage lang, int index, bool anywhere,
char *out, size_t n) {
const char *expr = spawn_arg_expr(call, index);
if (!expr) {
if (call->arg_count == 0 && index == 0 && call->first_string_arg) {
snprintf(out, n, "%s", call->first_string_arg);
return true;
}
return false;
}
if (anywhere ? spawn_first_literal(expr, out, n) : spawn_expr_literal(expr, out, n)) {
return true;
}
bool bareword = lang == CBM_LANG_TCL && strpbrk(expr, "$[") == NULL;
if (bareword || (call->first_string_arg && strcmp(expr, call->first_string_arg) == 0)) {
snprintf(out, n, "%s", expr);
return true;
}
return false;
}
/* A shell word that is not the program: an environment assignment
* (`LC_ALL=C make`) or the `env` that runs the program after them
* (`/usr/bin/env make`). */
static bool spawn_prefix_word(const char *word, size_t len) {
static const char env[] = "env";
size_t env_len = sizeof(env) - 1;
if (len >= env_len && strncmp(word + len - env_len, env, env_len) == 0 &&
(len == env_len || word[len - env_len - 1] == '/')) {
return true;
}
size_t i = 0;
while (i < len && spawn_word_char(word[i])) {
i++;
}
return i > 0 && i < len && word[i] == '=';
}
/* The program word of a command line: its first word that is not an
* environment prefix, without directories. Anything built at run time is not
* a program name. */
static void spawn_program_word(const char *literal, char *buf, size_t n) {
size_t len = 0;
for (;;) {
while (*literal == ' ' || *literal == '\t') {
literal++;
}
len = strcspn(literal, " \t\r\n");
if (!spawn_prefix_word(literal, len) || !literal[len]) {
break;
}
literal += len;
}
const char *start = literal;
for (size_t i = 0; i < len; i++) {
if (literal[i] == '/' || literal[i] == '\\') {
start = literal + i + 1;
}
}
len -= (size_t)(start - literal);
bool dynamic = len == 0 || len >= CBM_SPAWN_PROGRAM_MAX || len >= n;
for (size_t i = 0; i < len && !dynamic; i++) {
dynamic = strchr("$%{}`()<>|;&*?[]\"'", start[i]) != NULL;
}
if (dynamic) {
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
return;
}
memcpy(buf, start, len);
buf[len] = '\0';
}
void cbm_spawn_program(const cbm_spawn_api_t *api, CBMLanguage lang, const CBMCall *call, char *buf,
size_t n) {
char literal[CBM_SZ_256];
bool found = false;
if (api->rule == CBM_SPAWN_STRING_RECEIVER) {
found = spawn_first_literal(call->callee_name, literal, sizeof(literal));
} else {
bool anywhere = api->alt == CBM_SPAWN_ANY_LITERAL;
found = spawn_arg_literal(call, lang, api->arg, anywhere, literal, sizeof(literal)) ||
(api->alt >= 0 &&
spawn_arg_literal(call, lang, api->alt, false, literal, sizeof(literal)));
}
if (!found) {
snprintf(buf, n, "%s", CBM_SPAWN_DYNAMIC);
return;
}
spawn_program_word(literal, buf, n);
}