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 patterns = Patterns::new(language);
|
||||||
let mut scopes: Vec<Scope> = Vec::new();
|
let mut scopes: Vec<Scope> = Vec::new();
|
||||||
let mut brace_depth = 0_i32;
|
let mut brace_depth = 0_i32;
|
||||||
|
let mut comment_depth = 0_usize;
|
||||||
|
|
||||||
for (index, raw_line) in content.lines().enumerate() {
|
for (index, raw_line) in content.lines().enumerate() {
|
||||||
let line_number = index + 1;
|
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 call_line = mask_quoted_strings(line);
|
||||||
let indent = raw_line.len() - raw_line.trim_start().len();
|
let indent = raw_line.len() - raw_line.trim_start().len();
|
||||||
if language == "python" {
|
if language == "python" {
|
||||||
|
|
@ -479,6 +487,7 @@ impl ScanState {
|
||||||
let mut class_name = None;
|
let mut class_name = None;
|
||||||
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")
|
||||||
|
&& !(language == "swift" && is_swift_member_keyword(name.as_str()))
|
||||||
{
|
{
|
||||||
class_name = Some(name.as_str().to_owned());
|
class_name = Some(name.as_str().to_owned());
|
||||||
let owner_id = scopes
|
let owner_id = scopes
|
||||||
|
|
@ -2465,6 +2474,14 @@ struct Patterns {
|
||||||
impl Patterns {
|
impl Patterns {
|
||||||
fn new(language: &str) -> Self {
|
fn new(language: &str) -> Self {
|
||||||
let (class, function, import) = match language {
|
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" => (
|
"python" => (
|
||||||
r"^\s*class\s+(?P<name>[A-Za-z_]\w*)(?:\((?P<base>[A-Za-z_][\w.]*)\))?",
|
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*\(",
|
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 {
|
fn brace_delta(line: &str) -> i32 {
|
||||||
line.chars().fold(0, |depth, ch| match ch {
|
line.chars().fold(0, |depth, ch| match ch {
|
||||||
'{' => depth + 1,
|
'{' => 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