feat: establish first-class lexical language parity #2
5 changed files with 111 additions and 16 deletions
feat(scan): model Rust impl scopes and call forms
commit
ee8b7d2fb5
|
|
@ -14,7 +14,7 @@ contracts and fixtures reach the same standard.
|
||||||
| Language | Useful current coverage | Important gaps | Tier |
|
| Language | Useful current coverage | Important gaps | Tier |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Dart | Classes, mixins, enums, extensions, methods, named constructors, accessors, import/export/part directives, `extends`/`with`/`implements`/`on`, and ordinary calls | operators, generic relationship targets, conditional directives, and multiline declarations | first-class target |
|
| Dart | Classes, mixins, enums, extensions, methods, named constructors, accessors, import/export/part directives, `extends`/`with`/`implements`/`on`, and ordinary calls | operators, generic relationship targets, conditional directives, and multiline declarations | first-class target |
|
||||||
| Rust | Structs, enums, traits, ordinary/async/const/unsafe/extern functions, simple `use`, calls, and raw-string masking | impl/module ownership, unions/type aliases, macros, turbofish calls, and braced/glob imports | first-class target |
|
| Rust | Structs, enums, traits, ordinary/async/const/unsafe/extern functions, simple inherent/trait impl ownership, ordinary/turbofish/macro calls, braced `use` resolution, and raw-string masking | module ownership, unions/type aliases, nested generic impl headers, glob imports, and macro definitions | first-class target |
|
||||||
| C | Files, basic includes, structs, and narrow function forms through the shared C/C++ profile | distinct C identity, enums/typedefs/function pointers, multiline declarations, macros, and declaration masking | first-class target |
|
| C | Files, basic includes, structs, and narrow function forms through the shared C/C++ profile | distinct C identity, enums/typedefs/function pointers, multiline declarations, macros, and declaration masking | first-class target |
|
||||||
| C++ | Basic classes/structs, narrow function forms, calls, and local include resolution | templates, constructors/destructors/operators, multiple inheritance, namespaces, overloads, lambdas, qualifiers, and multiline declarations | first-class target |
|
| C++ | Basic classes/structs, narrow function forms, calls, and local include resolution | templates, constructors/destructors/operators, multiple inheritance, namespaces, overloads, lambdas, qualifiers, and multiline declarations | first-class target |
|
||||||
| JavaScript | Named functions, simple classes/extends, ES imports, and call-shaped expressions | methods, arrows/function expressions, CommonJS, dynamic imports, re-exports, decorators, and template literals | first-class target |
|
| JavaScript | Named functions, simple classes/extends, ES imports, and call-shaped expressions | methods, arrows/function expressions, CommonJS, dynamic imports, re-exports, decorators, and template literals | first-class target |
|
||||||
|
|
|
||||||
56
src/scan.rs
56
src/scan.rs
|
|
@ -279,6 +279,23 @@ impl ScanState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(pattern) = patterns.enclosing_scope.as_ref()
|
||||||
|
&& let Some(name) = pattern
|
||||||
|
.captures(line)
|
||||||
|
.and_then(|captures| captures.name("name"))
|
||||||
|
&& let Some(id) = self
|
||||||
|
.symbols_by_name
|
||||||
|
.get(&(language.into(), name.as_str().into()))
|
||||||
|
.and_then(|ids| (ids.len() == 1).then(|| ids[0].clone()))
|
||||||
|
&& self.node_kind(&id) == Some(NodeKind::Class)
|
||||||
|
{
|
||||||
|
scopes.push(Scope {
|
||||||
|
id,
|
||||||
|
indent,
|
||||||
|
brace_depth: brace_depth + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(captures) = patterns.class.captures(line)
|
if let Some(captures) = patterns.class.captures(line)
|
||||||
&& let Some(name) = captures.name("name")
|
&& let Some(name) = captures.name("name")
|
||||||
{
|
{
|
||||||
|
|
@ -365,7 +382,7 @@ impl ScanState {
|
||||||
if let Some(target) = captures.name("name").or_else(|| captures.name("name2")) {
|
if let Some(target) = captures.name("name").or_else(|| captures.name("name2")) {
|
||||||
self.pending.push(PendingRelationship {
|
self.pending.push(PendingRelationship {
|
||||||
source: file_id.into(),
|
source: file_id.into(),
|
||||||
target_name: clean_target(target.as_str()),
|
target_name: clean_import_target(target.as_str(), language),
|
||||||
kind: EdgeKind::Imports,
|
kind: EdgeKind::Imports,
|
||||||
language: language.into(),
|
language: language.into(),
|
||||||
member_call: false,
|
member_call: false,
|
||||||
|
|
@ -376,7 +393,11 @@ impl ScanState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for captures in patterns.call.captures_iter(&call_line) {
|
for captures in patterns
|
||||||
|
.call
|
||||||
|
.iter()
|
||||||
|
.flat_map(|pattern| pattern.captures_iter(&call_line))
|
||||||
|
{
|
||||||
let Some(target_match) = captures.name("name") else {
|
let Some(target_match) = captures.name("name") else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
@ -602,8 +623,9 @@ struct Patterns {
|
||||||
class: Regex,
|
class: Regex,
|
||||||
function: Vec<Regex>,
|
function: Vec<Regex>,
|
||||||
import: Regex,
|
import: Regex,
|
||||||
call: Regex,
|
call: Vec<Regex>,
|
||||||
relationships: Option<Regex>,
|
relationships: Option<Regex>,
|
||||||
|
enclosing_scope: Option<Regex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Patterns {
|
impl Patterns {
|
||||||
|
|
@ -651,6 +673,13 @@ impl Patterns {
|
||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
let mut calls = vec![r"(?P<name>[A-Za-z_]\w*(?:::\w+)*)\s*\("];
|
||||||
|
if language == "rust" {
|
||||||
|
calls.extend([
|
||||||
|
r"(?P<name>[A-Za-z_]\w*(?:::\w+)*)\s*::\s*<[^>]+>\s*\(",
|
||||||
|
r"(?P<name>[A-Za-z_]\w*(?:::\w+)*)!\s*\(",
|
||||||
|
]);
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
class: Regex::new(class).expect("class regex is static"),
|
class: Regex::new(class).expect("class regex is static"),
|
||||||
function: functions
|
function: functions
|
||||||
|
|
@ -658,10 +687,18 @@ impl Patterns {
|
||||||
.map(|pattern| Regex::new(pattern).expect("function regex is static"))
|
.map(|pattern| Regex::new(pattern).expect("function regex is static"))
|
||||||
.collect(),
|
.collect(),
|
||||||
import: Regex::new(import).expect("import regex is static"),
|
import: Regex::new(import).expect("import regex is static"),
|
||||||
call: Regex::new(r"(?P<name>[A-Za-z_]\w*(?:::\w+)*)\s*\(")
|
call: calls
|
||||||
.expect("call regex is static"),
|
.into_iter()
|
||||||
|
.map(|pattern| Regex::new(pattern).expect("call regex is static"))
|
||||||
|
.collect(),
|
||||||
relationships: relationships
|
relationships: relationships
|
||||||
.map(|pattern| Regex::new(pattern).expect("relationship regex is static")),
|
.map(|pattern| Regex::new(pattern).expect("relationship regex is static")),
|
||||||
|
enclosing_scope: (language == "rust").then(|| {
|
||||||
|
Regex::new(
|
||||||
|
r"^\s*impl(?:<[^>]*>)?\s+(?:(?:[A-Za-z_]\w*(?:::\w+)*)\s+for\s+)?(?P<name>[A-Za-z_]\w*)",
|
||||||
|
)
|
||||||
|
.expect("scope regex is static")
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -748,6 +785,15 @@ fn clean_target(value: &str) -> String {
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn clean_import_target(value: &str, language: &str) -> String {
|
||||||
|
let target = clean_target(value);
|
||||||
|
if language == "rust" {
|
||||||
|
target.trim_end_matches("::").into()
|
||||||
|
} else {
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn import_keys(name: &str, language: &str) -> Vec<String> {
|
fn import_keys(name: &str, language: &str) -> Vec<String> {
|
||||||
let mut keys = vec![name.to_owned()];
|
let mut keys = vec![name.to_owned()];
|
||||||
match language {
|
match language {
|
||||||
|
|
|
||||||
14
tests/fixtures/languages/rust/basic.rs
vendored
14
tests/fixtures/languages/rust/basic.rs
vendored
|
|
@ -1,12 +1,18 @@
|
||||||
use crate::support::Dependency;
|
mod support;
|
||||||
|
|
||||||
|
use crate::support::{helper, Dependency};
|
||||||
|
|
||||||
pub struct Worker;
|
pub struct Worker;
|
||||||
|
|
||||||
pub fn start() {
|
impl Worker {
|
||||||
helper();
|
pub fn start() {
|
||||||
|
helper();
|
||||||
|
generic::<usize>();
|
||||||
|
trace!();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn helper() {}
|
fn generic<T>() {}
|
||||||
|
|
||||||
pub const fn constant() -> usize {
|
pub const fn constant() -> usize {
|
||||||
1
|
1
|
||||||
|
|
|
||||||
3
tests/fixtures/languages/rust/support.rs
vendored
Normal file
3
tests/fixtures/languages/rust/support.rs
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub struct Dependency;
|
||||||
|
|
||||||
|
pub fn helper() {}
|
||||||
|
|
@ -27,11 +27,18 @@ fn first_class_language_fixtures_have_stable_baseline_evidence() {
|
||||||
FixtureCase {
|
FixtureCase {
|
||||||
directory: "rust",
|
directory: "rust",
|
||||||
language: "rust",
|
language: "rust",
|
||||||
classes: &["Worker"],
|
classes: &["Dependency", "Worker"],
|
||||||
functions: &["constant", "ffi_entry", "helper", "start", "unsafe_op"],
|
functions: &[
|
||||||
|
"constant",
|
||||||
|
"ffi_entry",
|
||||||
|
"generic",
|
||||||
|
"helper",
|
||||||
|
"start",
|
||||||
|
"unsafe_op",
|
||||||
|
],
|
||||||
imports: 1,
|
imports: 1,
|
||||||
inherits: 0,
|
inherits: 0,
|
||||||
calls: 1,
|
calls: 3,
|
||||||
},
|
},
|
||||||
FixtureCase {
|
FixtureCase {
|
||||||
directory: "c",
|
directory: "c",
|
||||||
|
|
@ -76,10 +83,37 @@ fn first_class_language_fixtures_have_stable_baseline_evidence() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rust_fixture_models_impl_ownership_and_local_imports() {
|
||||||
|
let root = fixture_root("rust");
|
||||||
|
let building = scan(&root).unwrap();
|
||||||
|
let worker = building
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.kind == NodeKind::Class && node.name == "Worker")
|
||||||
|
.unwrap();
|
||||||
|
let start = building
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.kind == NodeKind::Function && node.name == "start")
|
||||||
|
.unwrap();
|
||||||
|
let support = building
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.kind == NodeKind::File && node.path.as_deref() == Some("support.rs"))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(start.parent.as_deref(), Some(worker.id.as_str()));
|
||||||
|
assert!(
|
||||||
|
building
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.any(|edge| { edge.kind == EdgeKind::Imports && edge.target == support.id })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_fixture(case: FixtureCase) {
|
fn assert_fixture(case: FixtureCase) {
|
||||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
let root = fixture_root(case.directory);
|
||||||
.join("tests/fixtures/languages")
|
|
||||||
.join(case.directory);
|
|
||||||
let building = scan(&root).unwrap();
|
let building = scan(&root).unwrap();
|
||||||
let mut classes: Vec<_> = building
|
let mut classes: Vec<_> = building
|
||||||
.nodes
|
.nodes
|
||||||
|
|
@ -126,6 +160,12 @@ fn assert_fixture(case: FixtureCase) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fixture_root(directory: &str) -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("tests/fixtures/languages")
|
||||||
|
.join(directory)
|
||||||
|
}
|
||||||
|
|
||||||
fn edge_count(building: &brick::Building, kind: EdgeKind) -> usize {
|
fn edge_count(building: &brick::Building, kind: EdgeKind) -> usize {
|
||||||
building
|
building
|
||||||
.edges
|
.edges
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue