Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions crates/compass-core/src/pipeline.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use ahash::{AHashMap, AHashSet};
Expand All @@ -18,7 +18,8 @@ use compass_graph::{
};
use compass_languages::{
EXTRACTION_QUALITY_EXTENSION, EXTRACTION_QUALITY_PARTIAL, EXTRACTION_QUALITY_REASON_EXTENSION,
Engine, Extraction, ExtractorKind, RawEdgeRecord, RawNodeRecord, Registry, file_stem, make_id,
Engine, Extraction, ExtractorKind, FRAMEWORK_PROJECT_EVIDENCE_EXTENSION, ProjectEvidenceIndex,
RawEdgeRecord, RawNodeRecord, Registry, file_stem, make_id,
};
use compass_model::code_graph::{
DiagnosticSeverity, ExtractionStatus, GraphDiagnostic, GraphDocument as V1GraphDocument,
Expand Down Expand Up @@ -638,6 +639,7 @@ fn build_graph_inner(
CacheOptions::shared_history,
);
let mut cache = Cache::open(&root, cache_options)?;
let project_evidence = Arc::new(ProjectEvidenceIndex::build(&root, &sources));
let mut extractions = BTreeMap::<PathBuf, Extraction>::new();
let mut missing = Vec::new();
if reuse_cached_analysis {
Expand All @@ -658,7 +660,11 @@ fn build_graph_inner(
path: path.clone(),
source,
})?;
extractions.insert(path.clone(), extraction);
if cached_framework_evidence_matches(&extraction, path, &project_evidence) {
extractions.insert(path.clone(), extraction);
} else {
missing.push(path.clone());
}
} else {
missing.push(path.clone());
}
Expand Down Expand Up @@ -776,16 +782,20 @@ fn build_graph_inner(
Ok((path.clone(), combined.graph, source, prepared))
};
let fresh_outcomes = if missing.len() < 256 {
let mut engine = Engine::default();
let mut engine = Engine::with_project_evidence(Arc::clone(&project_evidence));
missing
.iter()
.map(|path| extract_source(&mut engine, path))
.collect::<Vec<_>>()
} else {
let worker_evidence = Arc::clone(&project_evidence);
let extract = || {
missing
.par_iter()
.map_init(Engine::default, extract_source)
.map_init(
|| Engine::with_project_evidence(Arc::clone(&worker_evidence)),
extract_source,
)
.collect::<Vec<_>>()
};
if let Some(pool) = &worker_pool {
Expand Down Expand Up @@ -3801,6 +3811,18 @@ fn read_source_text_with_limit(path: &Path, max_source_bytes: u64) -> Option<(St
})
}

fn cached_framework_evidence_matches(
extraction: &Extraction,
path: &Path,
project_evidence: &ProjectEvidenceIndex,
) -> bool {
extraction
.extensions
.get(FRAMEWORK_PROJECT_EVIDENCE_EXTENSION)
.and_then(serde_json::Value::as_str)
== Some(project_evidence.fingerprint_for(path))
}

#[cfg(test)]
mod tests {
use std::error::Error;
Expand Down Expand Up @@ -3836,6 +3858,47 @@ mod tests {
Ok(())
}

#[test]
fn framework_cache_reuse_is_scoped_to_project_evidence() -> Result<(), Box<dyn Error>> {
let directory = tempfile::tempdir()?;
let source = directory.path().join("src/routes/+page.svelte");
fs::create_dir_all(source.parent().ok_or("source has no parent")?)?;
fs::write(&source, "<h1>Home</h1>")?;
fs::write(
directory.path().join("package.json"),
r#"{"dependencies":{}}"#,
)?;

let initial = ProjectEvidenceIndex::build(directory.path(), std::slice::from_ref(&source));
let mut extraction = Extraction::default();
assert!(!cached_framework_evidence_matches(
&extraction,
&source,
&initial
));
extraction.extensions.insert(
FRAMEWORK_PROJECT_EVIDENCE_EXTENSION.to_owned(),
Value::String(initial.fingerprint_for(&source).to_owned()),
);
assert!(cached_framework_evidence_matches(
&extraction,
&source,
&initial
));

fs::write(
directory.path().join("package.json"),
r#"{"dependencies":{"@sveltejs/kit":"2.0.0"}}"#,
)?;
let changed = ProjectEvidenceIndex::build(directory.path(), std::slice::from_ref(&source));
assert!(!cached_framework_evidence_matches(
&extraction,
&source,
&changed
));
Ok(())
}

#[test]
fn precomputed_detection_cannot_cross_repository_roots() -> Result<(), Box<dyn Error>> {
let detected_root = tempfile::tempdir()?;
Expand Down
4 changes: 4 additions & 0 deletions crates/compass-core/tests/code_graph_v1_determinism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ fn production_pipeline_preserves_framework_domain_kinds_and_route_targets()
let directory = tempfile::tempdir()?;
let root = directory.path();
for (relative, source) in [
(
"package.json",
r#"{"dependencies":{"nuxt":"4.0.0","@nestjs/common":"11.0.0","react-router-dom":"7.0.0"}}"#,
),
(
"src/orders.ts",
r#"import { Controller } from '@nestjs/common';
Expand Down
75 changes: 60 additions & 15 deletions crates/compass-languages/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fs;
use std::fs::File;
use std::io::Read;
use std::path::Path;
use std::sync::Arc;

use crate::{RawEdgeRecord as EdgeRecord, RawNodeRecord as NodeRecord};
use serde_json::{Map, Value};
Expand All @@ -13,18 +14,28 @@ use crate::builtins::is_language_builtin_global;
use crate::config::{GenericConfig, generic_config};
use crate::{
CombinedExtraction, EXTRACTION_QUALITY_EXTENSION, EXTRACTION_QUALITY_PARTIAL,
EXTRACTION_QUALITY_REASON_EXTENSION, ExtractError, Extraction, ExtractorKind, LanguageSpec,
RawCall, Registry, file_stem, make_id,
EXTRACTION_QUALITY_REASON_EXTENSION, ExtractError, Extraction, ExtractorKind,
FRAMEWORK_PROJECT_EVIDENCE_EXTENSION, LanguageSpec, ProjectEvidenceIndex, RawCall, Registry,
file_stem, make_id,
};

const JSON_MAX_BYTES: u64 = 1_048_576;

#[derive(Default)]
pub struct Engine {
parsers: HashMap<&'static str, Parser>,
project_evidence: Option<Arc<ProjectEvidenceIndex>>,
}

impl Engine {
#[must_use]
pub fn with_project_evidence(project_evidence: Arc<ProjectEvidenceIndex>) -> Self {
Self {
parsers: HashMap::new(),
project_evidence: Some(project_evidence),
}
}

pub fn extract(&mut self, path: &Path) -> Result<Extraction, ExtractError> {
let spec =
Registry::resolve(path).ok_or_else(|| ExtractError::Unsupported(path.to_path_buf()))?;
Expand All @@ -44,7 +55,12 @@ impl Engine {
ExtractorKind::Template => {
let mut extraction = crate::templates::extract(self, path, spec.name)?;
if let Ok(source) = fs::read(path) {
crate::frameworks::detect_template_file_route(path, &source, &mut extraction);
crate::frameworks::detect_template_file_route(
path,
&source,
self.project_evidence(path),
&mut extraction,
);
}
Ok(extraction)
}
Expand All @@ -53,9 +69,14 @@ impl Engine {
path: path.to_path_buf(),
source,
})?;
Ok(crate::frameworks::detect_config_file(path, &source))
Ok(crate::frameworks::detect_config_file(
path,
&source,
self.project_evidence(path),
))
}
}?;
self.stamp_project_evidence(path, &mut extraction);
stamp_producer_metadata(&mut extraction, spec.name);
Ok(extraction)
}
Expand All @@ -74,11 +95,14 @@ impl Engine {
ExtractorKind::Generic => self.extract_generic_source(path, spec, source),
ExtractorKind::JsonConfig => self.extract_json_source(path, spec, source),
ExtractorKind::Terraform => self.extract_terraform_source(path, spec, source),
ExtractorKind::FrameworkConfig => {
Ok(crate::frameworks::detect_config_file(path, source))
}
ExtractorKind::FrameworkConfig => Ok(crate::frameworks::detect_config_file(
path,
source,
self.project_evidence(path),
)),
_ => self.extract(path),
}?;
self.stamp_project_evidence(path, &mut extraction);
stamp_producer_metadata(&mut extraction, spec.name);
Ok(extraction)
}
Expand Down Expand Up @@ -106,7 +130,8 @@ impl Engine {
}
let tree = self.parse(path, spec, source)?;
let root = tree.root_node();
let mut graph = Self::extract_generic_from_tree(path, spec, source, root);
let mut graph = self.extract_generic_from_tree(path, spec, source, root);
self.stamp_project_evidence(path, &mut graph);
stamp_producer_metadata(&mut graph, spec.name);
let program = crate::program::extract_from_tree(source_file, spec.name, source, root)
.map_err(|error| ExtractError::InvalidProgramEvidence {
Expand Down Expand Up @@ -149,6 +174,7 @@ impl Engine {
&generic_config(spec),
language,
);
self.stamp_project_evidence(path, &mut extraction);
stamp_producer_metadata(&mut extraction, language);
Ok(extraction)
}
Expand Down Expand Up @@ -203,15 +229,11 @@ impl Engine {
source
};
let tree = self.parse(path, spec, source)?;
Ok(Self::extract_generic_from_tree(
path,
spec,
source,
tree.root_node(),
))
Ok(self.extract_generic_from_tree(path, spec, source, tree.root_node()))
}

fn extract_generic_from_tree(
&self,
path: &Path,
spec: LanguageSpec,
source: &[u8],
Expand All @@ -238,7 +260,14 @@ impl Engine {
}
attach_definition_metadata(&mut extraction, source, root, &config, spec.name);
crate::semantic::enrich(path, source, root, spec.name, &mut extraction);
crate::frameworks::detect(path, source, root, spec.name, &mut extraction);
crate::frameworks::detect(
path,
source,
root,
spec.name,
self.project_evidence(path),
&mut extraction,
);
if root.has_error() {
extraction.extensions.insert(
EXTRACTION_QUALITY_EXTENSION.to_owned(),
Expand All @@ -252,6 +281,22 @@ impl Engine {
extraction
}

fn project_evidence(&self, path: &Path) -> Option<&crate::ProjectEvidence> {
self.project_evidence
.as_deref()
.map(|index| index.evidence_for(path))
}

fn stamp_project_evidence(&self, path: &Path, extraction: &mut Extraction) {
let Some(evidence) = self.project_evidence(path) else {
return;
};
extraction.extensions.insert(
FRAMEWORK_PROJECT_EVIDENCE_EXTENSION.to_owned(),
Value::String(evidence.fingerprint().to_owned()),
);
}

fn extract_json(
&mut self,
path: &Path,
Expand Down
16 changes: 15 additions & 1 deletion crates/compass-languages/src/frameworks/csharp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,26 @@ use regex::Regex;
use serde_json::Map;
use tree_sitter::Node;

use super::evidence::{EvidenceKind, EvidenceSet};
use super::text::{join_route_path, line_anchor, normalize_route_path, text};
use super::{RawFrameworkFact, RawFrameworkOrigin, RawRouteFact};

pub(super) fn detect(path: &Path, source: &[u8], _root: Node<'_>) -> Vec<RawFrameworkFact> {
let body = text(source);
if !body.contains("Microsoft.AspNetCore.Mvc") && !body.contains("[ApiController]") {
let evidence = EvidenceSet::new()
.direct_if(
body.contains("Microsoft.AspNetCore.Mvc"),
"aspnet",
EvidenceKind::Import,
"Microsoft.AspNetCore.Mvc",
)
.supporting_if(
body.contains("[ApiController]"),
"aspnet",
EvidenceKind::DecoratorOrAttribute,
"ApiController",
);
if !evidence.activates("aspnet") {
return Vec::new();
}
let Ok(route_attribute) = Regex::new(r#"\[Route\(\s*"([^"]*)"\s*\)\]"#) else {
Expand Down
Loading
Loading