pub mod coverage;
pub mod detect;
pub mod functions;
pub mod imports;
pub mod module_map;
pub mod source;
use fxrank_core::CorpusProfile;
use fxrank_core::frontend::{Frontend, FrontendOutput, Language, SourceFile};
use fxrank_core::model::Diagnostic;
use libcst_native::parse_module;
pub const CORPUS_PROFILE: CorpusProfile = CorpusProfile {
prune_dirs: &[
".venv",
"venv",
".tox",
".nox",
"__pycache__",
".eggs",
"build",
"dist",
".mypy_cache",
".pytest_cache",
".ruff_cache",
"site-packages",
],
exclude_file_globs: &["*_pb2.py", "*_pb2_grpc.py"],
test_file_globs: &["test_*.py", "*_test.py", "conftest.py", "tests"],
prune_marker_files: &["pyvenv.cfg"],
};
pub struct PythonFrontend {
pub include_tests: bool,
}
impl Frontend for PythonFrontend {
fn language(&self) -> Language {
Language::Python
}
fn corpus_profile(&self) -> CorpusProfile {
CORPUS_PROFILE
}
fn analyze(&self, files: &[SourceFile]) -> FrontendOutput {
let module_map = module_map::PyModuleMap::build(files);
let mut output = FrontendOutput::default();
for file in files {
let src = source::strip_bom(&file.text);
match parse_module(src, None) {
Err(e) => {
output.diagnostics.push(Diagnostic {
path: file.path.clone(),
parsed: false,
error: format!("{e}"),
});
}
Ok(module) => {
let imports = imports::Imports::build(&module);
let module_bindings = crate::imports::module_bindings(&module);
let span = source::SpanIndex::new(src);
let anchors = match source::lambda_anchors(src) {
Some(a) => a,
None => {
output.diagnostics.push(Diagnostic {
path: file.path.clone(),
parsed: true,
error: "lambda anchoring unavailable: tokenizer failed on \
a file that parsed successfully; hotspots for this \
file are omitted to avoid mis-anchored output"
.into(),
});
continue;
}
};
let (units, lambda_node_count) =
functions::collect(&module, src, &span, &anchors);
{
let anchor_count = anchors.len();
if lambda_node_count != anchor_count {
output.diagnostics.push(Diagnostic {
path: file.path.clone(),
parsed: true,
error: format!(
"lambda-anchor mismatch: CST walk found {lambda_node_count} Lambda node(s) \
but tokenizer found {anchor_count} lambda keyword(s); \
hotspots for this file are omitted to avoid mis-anchored output"
),
});
continue;
}
}
if !self.include_tests && is_test_file(&file.path) {
output.skipped_tests += units.len();
} else {
for unit in &units {
if !self.include_tests && unit.is_test_unit {
output.skipped_tests += 1;
} else {
output.functions.push(detect::analyze_unit(
unit,
&file.path,
&imports,
&module_bindings,
&span,
));
output.records.push(detect::build_record(
unit,
&file.path,
&imports,
&module_bindings,
&span,
&module_map,
));
}
}
if let Some(init_unit) = functions::module_init_unit(&module) {
let h = detect::analyze_unit(
&init_unit,
&file.path,
&imports,
&module_bindings,
&span,
);
if !h.effects.is_empty() {
let rec = detect::build_record(
&init_unit,
&file.path,
&imports,
&module_bindings,
&span,
&module_map,
);
output.records.push(rec);
output.functions.push(h);
}
}
}
}
}
}
output
}
}
pub fn is_test_file(path: &str) -> bool {
use std::sync::OnceLock;
static M: OnceLock<fxrank_core::CorpusMatcher> = OnceLock::new();
M.get_or_init(|| fxrank_core::CorpusMatcher::test_matcher(CORPUS_PROFILE.test_file_globs))
.matches_test_file(path)
}
#[cfg(test)]
mod tests {
use super::*;
fn analyze_files(paths: &[&str], include_tests: bool) -> FrontendOutput {
let files: Vec<SourceFile> = paths
.iter()
.map(|p| {
let text =
std::fs::read_to_string(p).unwrap_or_else(|e| panic!("cannot read {p}: {e}"));
SourceFile {
path: p.to_string(),
text,
}
})
.collect();
PythonFrontend { include_tests }.analyze(&files)
}
fn analyze_fixture_as(
fixture: &str,
logical_path: &str,
include_tests: bool,
) -> FrontendOutput {
let text = std::fs::read_to_string(fixture)
.unwrap_or_else(|e| panic!("cannot read {fixture}: {e}"));
PythonFrontend { include_tests }.analyze(&[SourceFile {
path: logical_path.to_string(),
text,
}])
}
#[test]
fn skips_test_code_by_default_and_counts() {
let out = analyze_files(
&["tests/fixtures/test_sample.py"],
false,
);
assert_eq!(out.functions.len(), 0);
assert!(out.skipped_tests >= 1);
let inc = analyze_files(
&["tests/fixtures/test_sample.py"],
true,
);
assert!(inc.functions.len() >= 3);
}
#[test]
fn source_based_skip_independent_of_path_skip() {
let out = analyze_fixture_as(
"tests/fixtures/mixed_tests.py",
"src/mixed_tests.py",
false,
);
let symbols: Vec<&str> = out.functions.iter().map(|h| h.symbol.as_str()).collect();
assert!(
symbols.contains(&"normal_function"),
"normal_function must not be skipped; got: {symbols:?}"
);
assert!(
!symbols.contains(&"test_something"),
"test_something must be skipped; got: {symbols:?}"
);
assert!(
!symbols.contains(&"test_render"),
"test_render must be skipped; got: {symbols:?}"
);
assert!(
!symbols.contains(&"helper"),
"helper must be skipped (method of Test* class TestWidget); got: {symbols:?}"
);
assert!(
!symbols.contains(&"test_case"),
"test_case must be skipped; got: {symbols:?}"
);
assert!(
out.skipped_tests >= 1,
"expected skipped_tests >= 1; got: {}",
out.skipped_tests
);
let inc = analyze_fixture_as(
"tests/fixtures/mixed_tests.py",
"src/mixed_tests.py",
true,
);
let inc_symbols: Vec<&str> = inc.functions.iter().map(|h| h.symbol.as_str()).collect();
assert!(
inc_symbols.contains(&"test_something"),
"with include_tests, test_something must be scored; got: {inc_symbols:?}"
);
}
#[test]
fn corpus_profile_method_returns_const() {
use fxrank_core::frontend::Frontend;
let p = PythonFrontend {
include_tests: false,
}
.corpus_profile();
assert_eq!(p.prune_dirs, CORPUS_PROFILE.prune_dirs);
assert_eq!(p.test_file_globs, CORPUS_PROFILE.test_file_globs);
}
#[test]
fn is_test_file_characterization() {
for p in [
"test_views.py",
"views_test.py",
"conftest.py",
"pkg/tests/helpers.py",
"tests/x.py",
] {
assert!(is_test_file(p), "expected test file: {p}");
}
for p in [
"views.py",
"pkg/mytests/foo.py",
"tests.py",
"contest.py",
"test_views.txt",
] {
assert!(!is_test_file(p), "expected NON-test file: {p}");
}
}
#[test]
fn false_resolve_killed() {
use fxrank_core::record::CallSiteRef;
use fxrank_core::resolve::{CanonicalIndex, resolve_ref_precise};
let src = "\
from subprocess import run
def run():
pass
def caller():
run(['ls'])
";
let file_path = "app.py";
let files = vec![SourceFile {
path: file_path.to_string(),
text: src.to_string(),
}];
let out = PythonFrontend {
include_tests: false,
}
.analyze(&files);
assert!(
out.diagnostics.is_empty(),
"unexpected parse error: {:?}",
out.diagnostics
);
let idx = CanonicalIndex::from_records(&out.records);
assert!(
idx.adopted(),
"index must be adopted: the local `def run` must carry a canonical_path"
);
let caller_rec = out
.records
.iter()
.find(|r| r.symbol == "caller")
.expect("caller record not found");
let run_ref: &CallSiteRef = caller_rec
.refs
.iter()
.find(|r| r.base == "run")
.expect("expected a ref with base 'run' in caller");
assert!(
run_ref.qualified,
"run ref must be qualified=true (imported from subprocess)"
);
assert_eq!(
run_ref.resolved_target, None,
"subprocess.run is not in-batch → resolved_target must be None"
);
let edge = resolve_ref_precise(run_ref, &idx, file_path);
let is_opaque = matches!(edge, Some(fxrank_core::graph::Edge::Opaque(_)));
assert!(
is_opaque,
"subprocess.run must resolve to Edge::Opaque (stdlib), \
not Edge::Resolved to the local `def run`; got: {edge:?}",
edge = if matches!(edge, Some(fxrank_core::graph::Edge::Resolved(_))) {
"Edge::Resolved (FALSE RESOLVE — BUG)"
} else if edge.is_none() {
"None"
} else {
"Edge::Opaque (correct)"
}
);
}
}