mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-17 17:39:44 +00:00
Record a bash script's invocation of another script as an edge
The bash extractor recorded functions, source imports, and calls
between functions, but nothing for the programs a script actually
runs. In a pipeline driven by a shell script that is most of what the
script does, so an orchestrator looked disconnected from every stage
it runs: path between the runner and a stage it invokes found
nothing.
Add an invokes edge when a command's first word is a known
interpreter (python, python3, python2, node, nodejs, ruby, perl) or a
bare variable standing in for one, and an argument resolves to an
existing file in the repo with a recognized script extension. Kept
entirely separate from the existing bash to bash runner handling
(bash/sh/zsh/ksh/dash plus direct dot slash invocation), which still
emits its own calls edge unchanged, so this only adds coverage rather
than risking the existing path.
Gated on the target actually existing on disk, same as every other
heuristic edge in this file, so a plain command that merely happens to
take a dot py argument (cp, a custom tool) is never mistaken for an
interpreter invocation.
Fixes #3802.
(cherry picked from commit 52fe75be93)
This commit is contained in:
1 parent
c96a743b0c
commit
0d2ade622d
1 file changed
+42
@@ -174,6 +174,13 @@ def extract_bash(path: Path) -> dict:
|
||||
|
||||
_BASH_SOURCE_COMMANDS = frozenset({"source", "."})
|
||||
_BASH_SCRIPT_RUNNERS = frozenset({"bash", "sh", "zsh", "ksh", "dash"})
|
||||
# Non-shell interpreters a pipeline orchestrator commonly hands a script
|
||||
# path to (#3802): `python3 scripts/stage_one.py`, `node build.js`. A
|
||||
# script run this way is most of what an orchestrator does, and none of
|
||||
# it produced an edge before — the orchestrator looked disconnected from
|
||||
# every stage it runs.
|
||||
_SCRIPT_INVOKERS = frozenset({"python", "python3", "python2", "node", "nodejs", "ruby", "perl"})
|
||||
_INVOKABLE_SCRIPT_EXTENSIONS = (".py", ".js", ".mjs", ".cjs", ".rb", ".pl", ".sh")
|
||||
# Parent node types that mean a contained command is part of a substitution
|
||||
# or expansion, not a real function call. Token-level filtering misses
|
||||
# these because `$(build)` exposes `build` as a child command whose name
|
||||
@@ -511,6 +518,41 @@ def extract_bash(path: Path) -> dict:
|
||||
"calls", node.start_point[0] + 1,
|
||||
context="script_invocation",
|
||||
target_file=str(target_path))
|
||||
# python3 scripts/stage_one.py / node build.js / "$PYTHON"
|
||||
# scripts/stage_two.py (#3802): a script run through a
|
||||
# non-shell interpreter, or through a bare variable standing
|
||||
# in for one, is still a real dependency on that file -
|
||||
# independent of the .sh-specific runner/direct-invocation
|
||||
# handling above, so this never interferes with it.
|
||||
is_known_interpreter = (
|
||||
cmd is not None and cmd in _SCRIPT_INVOKERS
|
||||
and cmd not in defined_functions
|
||||
)
|
||||
cmd_word = text(cmd_name_node).strip()
|
||||
if cmd_word[0:1] in {"'", '"'} and cmd_word[-1:] == cmd_word[0]:
|
||||
cmd_word = cmd_word[1:-1]
|
||||
is_interpreter_variable = (
|
||||
cmd is None and cmd_word.startswith("$") and "$(" not in cmd_word
|
||||
)
|
||||
if (is_known_interpreter or is_interpreter_variable) and args:
|
||||
invoked_raw = literal(args[0])
|
||||
if invoked_raw and invoked_raw.endswith(_INVOKABLE_SCRIPT_EXTENSIONS):
|
||||
resolved = (path.parent / invoked_raw).resolve()
|
||||
if resolved.is_file():
|
||||
target_path = resolved
|
||||
if not path.is_absolute():
|
||||
try:
|
||||
target_path = resolved.relative_to(Path.cwd().resolve())
|
||||
except ValueError:
|
||||
pass
|
||||
caller_nid = entry_nid if parent_nid == file_nid else parent_nid
|
||||
tgt_nid = _make_id(str(target_path))
|
||||
if resolved.suffix == ".sh":
|
||||
tgt_nid += "__entry"
|
||||
add_edge(caller_nid, tgt_nid, "invokes",
|
||||
node.start_point[0] + 1,
|
||||
confidence="INFERRED", context="script_invocation",
|
||||
target_file=str(target_path))
|
||||
return
|
||||
|
||||
if t == "declaration_command":
|
||||
|
||||
Reference in new issue
Block a user