fix: review follow-ups for #5 and the Swift lexical preview #8
2 changed files with 179 additions and 1 deletions
fix(swift): give the lexical preview Swift's forms and mask block comments
Swift ran through the C-style lexical patterns, which never recognize
`func`. On a small file (`func realTarget()`, `func ghost()`, a class
method `caller` that calls `realTarget()` below a `/* ghost() */`
comment) the preview reported:
a.swift -> ghost (a.swift:2 declaration, a.swift:5 in a comment)
a.swift -> caller (its declaration line)
a.swift -> realTarget (its declaration line)
and missed the real call. Now:
caller -> realTarget (a.swift:7)
- Swift gets its own patterns: `func` declarations (with attributes and
modifiers), class/struct/enum/protocol/actor/extension declarations
(`class func` / `class var` are members, not classes), and imports.
- Block comments are masked across lines, nested as Swift allows, and a
`/*` inside a string literal does not open one. The masker is salvaged
from the unmerged language-parity branch (543bafc).
- Calls are attributed to their enclosing function through the existing
brace-scoped scopes, now that functions are recognized.
Test: tests/swift_preview.rs (functions and a class recognized, the call
attributed to `caller`, nothing from comments, nothing from the file
node); it failed before this change.
commit
c3c7c6c3f4
82
src/scan.rs
82
src/scan.rs
|
|
@ -452,10 +452,18 @@ impl ScanState {
|
|||
let patterns = Patterns::new(language);
|
||||
let mut scopes: Vec<Scope> = Vec::new();
|
||||
let mut brace_depth = 0_i32;
|
||||
let mut comment_depth = 0_usize;
|
||||
|
||||
for (index, raw_line) in content.lines().enumerate() {
|
||||
let line_number = index + 1;
|
||||
let line = strip_line_comment(raw_line, language);
|
||||
// Block comments (nested, as Swift allows) span lines; without masking,
|
||||
// a call inside `/* ... */` became an edge.
|
||||
let masked = if language == "python" {
|
||||
raw_line.to_owned()
|
||||
} else {
|
||||
mask_block_comments(raw_line, &mut comment_depth)
|
||||
};
|
||||
let line = strip_line_comment(&masked, language);
|
||||
let call_line = mask_quoted_strings(line);
|
||||
let indent = raw_line.len() - raw_line.trim_start().len();
|
||||
if language == "python" {
|
||||
|
|
@ -479,6 +487,7 @@ impl ScanState {
|
|||
let mut class_name = None;
|
||||
if let Some(captures) = patterns.class.captures(line)
|
||||
&& let Some(name) = captures.name("name")
|
||||
&& !(language == "swift" && is_swift_member_keyword(name.as_str()))
|
||||
{
|
||||
class_name = Some(name.as_str().to_owned());
|
||||
let owner_id = scopes
|
||||
|
|
@ -2465,6 +2474,14 @@ struct Patterns {
|
|||
impl Patterns {
|
||||
fn new(language: &str) -> Self {
|
||||
let (class, function, import) = match language {
|
||||
// Swift's own forms: `func name(`, type declarations with modifiers and
|
||||
// attributes, and module imports. It used to run through the C patterns,
|
||||
// which never recognized `func`.
|
||||
"swift" => (
|
||||
r"^\s*(?:@\w+(?:\([^)]*\))?\s+)*(?:(?:public|private|internal|fileprivate|open|final|indirect)\s+)*(?:class|struct|enum|protocol|actor|extension)\s+(?P<name>[A-Za-z_]\w*)(?:\s*:\s*(?P<base>[A-Za-z_][\w.]*))?",
|
||||
r"^\s*(?:@\w+(?:\([^)]*\))?\s+)*(?:(?:public|private|internal|fileprivate|open|static|class|override|mutating|nonmutating|final|required|convenience|dynamic|nonisolated)\s+)*func\s+(?P<name>[A-Za-z_]\w*)",
|
||||
r"^\s*(?:@\w+\s+)?import\s+(?:(?:typealias|struct|class|enum|protocol|let|var|func)\s+)?(?P<name>[A-Za-z_][\w.]*)",
|
||||
),
|
||||
"python" => (
|
||||
r"^\s*class\s+(?P<name>[A-Za-z_]\w*)(?:\((?P<base>[A-Za-z_][\w.]*)\))?",
|
||||
r"^\s*(?:async\s+)?def\s+(?P<name>[A-Za-z_]\w*)\s*\(",
|
||||
|
|
@ -2509,6 +2526,69 @@ fn strip_line_comment<'a>(line: &'a str, language: &str) -> &'a str {
|
|||
}
|
||||
}
|
||||
|
||||
/// `class func` / `class var` declare members, not a class named `func`.
|
||||
fn is_swift_member_keyword(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"func" | "var" | "let" | "subscript" | "init" | "deinit" | "case" | "typealias"
|
||||
)
|
||||
}
|
||||
|
||||
/// Mask `/* ... */` comments (nested, as Swift allows) with spaces, carrying the
|
||||
/// open-comment depth across lines. Quoted strings are left intact, so a `/*`
|
||||
/// inside a string (`"http://x/*y"`) does not open a comment.
|
||||
fn mask_block_comments(line: &str, depth: &mut usize) -> String {
|
||||
let chars: Vec<char> = line.chars().collect();
|
||||
let mut result = String::with_capacity(line.len());
|
||||
let mut quote = None;
|
||||
let mut escaped = false;
|
||||
let mut index = 0;
|
||||
while index < chars.len() {
|
||||
let ch = chars[index];
|
||||
let next = chars.get(index + 1).copied();
|
||||
if *depth > 0 {
|
||||
if ch == '/' && next == Some('*') {
|
||||
*depth += 1;
|
||||
result.push_str(" ");
|
||||
index += 2;
|
||||
} else if ch == '*' && next == Some('/') {
|
||||
*depth -= 1;
|
||||
result.push_str(" ");
|
||||
index += 2;
|
||||
} else {
|
||||
result.push(' ');
|
||||
index += 1;
|
||||
}
|
||||
} else if let Some(active) = quote {
|
||||
result.push(ch);
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if ch == '\\' {
|
||||
escaped = true;
|
||||
} else if ch == active {
|
||||
quote = None;
|
||||
}
|
||||
index += 1;
|
||||
} else if ch == '"' && chars[index + 1..].contains(&'"') {
|
||||
quote = Some(ch);
|
||||
result.push(ch);
|
||||
index += 1;
|
||||
} else if ch == '/' && next == Some('/') {
|
||||
// The rest is a line comment; strip_line_comment removes it.
|
||||
result.extend(&chars[index..]);
|
||||
break;
|
||||
} else if ch == '/' && next == Some('*') {
|
||||
*depth = 1;
|
||||
result.push_str(" ");
|
||||
index += 2;
|
||||
} else {
|
||||
result.push(ch);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn brace_delta(line: &str) -> i32 {
|
||||
line.chars().fold(0, |depth, ch| match ch {
|
||||
'{' => depth + 1,
|
||||
|
|
|
|||
98
tests/swift_preview.rs
Normal file
98
tests/swift_preview.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
use brick::model::{Building, EdgeKind, NodeKind};
|
||||
use brick::scan;
|
||||
use std::fs;
|
||||
|
||||
fn name_of<'a>(model: &'a Building, id: &str) -> &'a str {
|
||||
model
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == id)
|
||||
.map_or("", |node| node.name.as_str())
|
||||
}
|
||||
|
||||
fn calls(model: &Building) -> Vec<(String, String, NodeKind)> {
|
||||
model
|
||||
.edges
|
||||
.iter()
|
||||
.filter(|edge| edge.kind == EdgeKind::Calls)
|
||||
.map(|edge| {
|
||||
let source_kind = model
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.id == edge.source)
|
||||
.map(|node| node.kind)
|
||||
.unwrap();
|
||||
(
|
||||
name_of(model, &edge.source).to_owned(),
|
||||
name_of(model, &edge.target).to_owned(),
|
||||
source_kind,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swift_preview_declares_functions_and_ignores_comments() {
|
||||
// Swift ran through the C-style lexical patterns: `func` declarations were
|
||||
// never functions, each declaration line became a call from the file, calls
|
||||
// inside (nested) block comments became edges, and real calls were attributed
|
||||
// to the file instead of their function.
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(
|
||||
dir.path().join("a.swift"),
|
||||
r#"import Foundation
|
||||
|
||||
func realTarget() -> Int { return 1 }
|
||||
func ghost() -> Int { return 2 }
|
||||
|
||||
final class Worker: NSObject {
|
||||
let url = "http://example.com/*not-a-comment"
|
||||
func caller() -> Int {
|
||||
/* disabled:
|
||||
ghost()
|
||||
/* nested: ghost() */
|
||||
still disabled: ghost()
|
||||
*/
|
||||
return realTarget()
|
||||
}
|
||||
}
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let model = scan::scan(dir.path()).unwrap();
|
||||
|
||||
let functions: Vec<&str> = model
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| node.kind == NodeKind::Function)
|
||||
.map(|node| node.name.as_str())
|
||||
.collect();
|
||||
for name in ["realTarget", "ghost", "caller"] {
|
||||
assert!(
|
||||
functions.contains(&name),
|
||||
"{name} not a function: {functions:?}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
model
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|node| node.kind == NodeKind::Class && node.name == "Worker")
|
||||
);
|
||||
|
||||
let calls = calls(&model);
|
||||
assert!(
|
||||
calls.iter().any(|(source, target, kind)| source == "caller"
|
||||
&& target == "realTarget"
|
||||
&& *kind == NodeKind::Function),
|
||||
"{calls:?}"
|
||||
);
|
||||
assert!(
|
||||
!calls.iter().any(|(_, target, _)| target == "ghost"),
|
||||
"a call inside a block comment became an edge: {calls:?}"
|
||||
);
|
||||
assert!(
|
||||
!calls.iter().any(|(_, _, kind)| *kind == NodeKind::File),
|
||||
"a declaration or statement was attributed to the file: {calls:?}"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue