mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
refactor(dynamic): add cross-file route detection for frameworks, enhance test coverage in PHP and Ruby
This commit is contained in:
parent
43ab4aa469
commit
0e8c900078
22 changed files with 1208 additions and 134 deletions
|
|
@ -20,7 +20,7 @@
|
|||
use crate::callgraph::{CallGraph, CallGraphAnalysis};
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::dynamic::corpus::CORPUS_VERSION;
|
||||
use crate::dynamic::framework::FrameworkBinding;
|
||||
use crate::dynamic::framework::{FrameworkBinding, FrameworkDetectionContext, ProjectFileIndex};
|
||||
use crate::dynamic::stubs::StubKind;
|
||||
use crate::evidence::{Confidence, FlowStepKind, UnsupportedReason};
|
||||
use crate::labels::Cap;
|
||||
|
|
@ -28,7 +28,7 @@ use crate::summary::{FuncSummary, GlobalSummaries};
|
|||
use crate::symbol::{FuncKey, Lang};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Re-export of the always-present [`crate::evidence::SpecDerivationStrategy`].
|
||||
///
|
||||
|
|
@ -1241,9 +1241,14 @@ fn attach_framework_binding(spec: &mut HarnessSpec, summaries: Option<&GlobalSum
|
|||
let summary_ref = resolved.unwrap_or(&synthetic);
|
||||
let ssa_ref = summaries
|
||||
.and_then(|gs| find_ssa_summary_by_path(gs, spec.lang, &spec.entry_name, &spec.entry_file));
|
||||
if let Some(binding) = crate::dynamic::framework::detect_binding_with_context(
|
||||
let project_files = framework_project_files_for_entry(&spec.entry_file, spec.lang);
|
||||
let context = FrameworkDetectionContext {
|
||||
ssa_summary: ssa_ref,
|
||||
project_files: &project_files,
|
||||
};
|
||||
if let Some(binding) = crate::dynamic::framework::detect_binding_with_project_context(
|
||||
summary_ref,
|
||||
ssa_ref,
|
||||
context,
|
||||
tree.root_node(),
|
||||
&bytes,
|
||||
spec.lang,
|
||||
|
|
@ -1252,6 +1257,43 @@ fn attach_framework_binding(spec: &mut HarnessSpec, summaries: Option<&GlobalSum
|
|||
}
|
||||
}
|
||||
|
||||
fn framework_project_files_for_entry(entry_file: &str, lang: Lang) -> ProjectFileIndex {
|
||||
let Some(root) = infer_framework_project_root(Path::new(entry_file), lang) else {
|
||||
return ProjectFileIndex::new();
|
||||
};
|
||||
let rel_paths: &[&str] = match lang {
|
||||
Lang::Ruby => &["config/routes.rb"],
|
||||
Lang::Php => &[
|
||||
"config/routes.yaml",
|
||||
"config/routes.yml",
|
||||
"routes/web.php",
|
||||
"routes/api.php",
|
||||
"app/Config/Routes.php",
|
||||
],
|
||||
_ => &[],
|
||||
};
|
||||
ProjectFileIndex::from_root(&root, rel_paths)
|
||||
}
|
||||
|
||||
fn infer_framework_project_root(entry_path: &Path, lang: Lang) -> Option<PathBuf> {
|
||||
let dirs: &[&str] = match lang {
|
||||
Lang::Ruby => &["app"],
|
||||
Lang::Php => &["src", "app"],
|
||||
_ => &[],
|
||||
};
|
||||
for ancestor in entry_path.ancestors() {
|
||||
let Some(name) = ancestor.file_name().and_then(|n| n.to_str()) else {
|
||||
continue;
|
||||
};
|
||||
if dirs.contains(&name)
|
||||
&& let Some(parent) = ancestor.parent()
|
||||
{
|
||||
return Some(parent.to_path_buf());
|
||||
}
|
||||
}
|
||||
entry_path.parent().map(|p| p.to_path_buf())
|
||||
}
|
||||
|
||||
/// Phase 18 (Track M.0) — apply a resolved [`FrameworkBinding`] onto
|
||||
/// the spec. Carved out of [`attach_framework_binding`] so the
|
||||
/// stamping branch (Phase 18 data-bearing-variant propagation +
|
||||
|
|
@ -2227,6 +2269,53 @@ mod tests {
|
|||
assert_eq!(spec_no_summaries.spec_hash, spec_with_summaries.spec_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attach_framework_binding_reads_project_route_config() {
|
||||
use crate::dynamic::framework::HttpMethod;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let action_dir = dir.path().join("app/actions/books");
|
||||
fs::create_dir_all(&action_dir).expect("action dir");
|
||||
let config_dir = dir.path().join("config");
|
||||
fs::create_dir_all(&config_dir).expect("config dir");
|
||||
let action = action_dir.join("show.rb");
|
||||
fs::File::create(&action)
|
||||
.expect("action create")
|
||||
.write_all(
|
||||
b"require 'hanami/action'\nmodule Books\n class Show\n include Hanami::Action\n def call(req)\n system(req.params[:cmd])\n end\n end\nend\n",
|
||||
)
|
||||
.expect("action write");
|
||||
fs::File::create(config_dir.join("routes.rb"))
|
||||
.expect("routes create")
|
||||
.write_all(b"Hanami.app.routes do\n post '/books/:id', to: 'books.show'\nend\n")
|
||||
.expect("routes write");
|
||||
let entry_file = action.to_string_lossy().into_owned();
|
||||
|
||||
let ev = Evidence {
|
||||
flow_steps: vec![source_step(&entry_file, "call"), sink_step(&entry_file)],
|
||||
sink_caps: Cap::SHELL_ESCAPE.bits(),
|
||||
..Default::default()
|
||||
};
|
||||
let diag = crate::commands::scan::Diag {
|
||||
id: "rb.cmdi.system".into(),
|
||||
path: entry_file.clone(),
|
||||
line: 5,
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(ev),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let spec = HarnessSpec::from_finding_full(&diag, false, None, None)
|
||||
.expect("spec derives and attaches framework config");
|
||||
let binding = spec.framework.expect("hanami binding");
|
||||
assert_eq!(binding.adapter, "ruby-hanami");
|
||||
let route = binding.route.expect("route");
|
||||
assert_eq!(route.method, HttpMethod::POST);
|
||||
assert_eq!(route.path, "/books/:id");
|
||||
}
|
||||
|
||||
/// Phase 18 (Track M.0) deferred-fix: when a [`FrameworkBinding`]
|
||||
/// carries one of the seven data-bearing variants
|
||||
/// (`ClassMethod`, `MessageHandler`, …), the spec stamping path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue