Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
5 changes: 4 additions & 1 deletion .repowiseIgnore
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ crates/root_tui_main_entry.rs
# module's mod.rs/lib.rs; that is intrinsic to the AES layered architecture,
# not removable duplication (merging would break the 327 dependents).
# Known repowise false positive — exclude from health scoring.
# Only verified-pure barrels are excluded; lib.rs carrying real logic is
# re-included below so its health is still scored. build.rs is NOT excluded
# (build scripts contain real logic).
**/mod.rs
**/lib.rs
**/build.rs
!crates/external-lint/src/lib.rs

68 changes: 55 additions & 13 deletions crates/shared/src/common/utility_signature_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,48 +184,88 @@ pub fn python_signature_uses_forbidden_primitive(sig: &str) -> Vec<&'static str>

/// Collect forbidden primitive type tokens from the parameter section.
fn collect_python_param_primitives(lower: &str, forbidden: &mut Vec<&'static str>) {
if lower.contains(": str") {
// Whole-token matching: `: int` fires, `: internal_id` does not.
if contains_token_prefix(lower, ": str") {
forbidden.push("str");
}
if lower.contains(": int") {
if contains_token_prefix(lower, ": int") {
forbidden.push("int");
}
if lower.contains(": float") {
if contains_token_prefix(lower, ": float") {
forbidden.push("float");
}
// Only flag bare `list`/`dict` without type parameters (e.g., `List[ResultVO]` is OK)
if lower.contains(": list") && !lower.contains(": list[") {
// Only flag bare `list`/`dict` without type parameters. `list [ResultVO]`
// (whitespace before the bracket) is parameterized, not bare.
if contains_bare_token(lower, ": list") {
forbidden.push("list");
}
if lower.contains(": dict") && !lower.contains(": dict[") {
if contains_bare_token(lower, ": dict") {
forbidden.push("dict");
}
}

/// True when `haystack` contains `prefix` as a whole token that is NOT
/// followed by a generic bracket (`[`) — i.e. a bare `list`/`dict`.
fn contains_bare_token(haystack: &str, prefix: &str) -> bool {
let prefix_len = prefix.len();
haystack.match_indices(prefix).any(|(i, _)| {
let Some(rest) = haystack.get(i + prefix_len..) else {
return true; // prefix at end of string — bare
};
let Some(next) = rest.chars().next() else {
return true; // prefix at end of string — bare
};
!next.is_alphanumeric() && next != '_' && next != '['
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// True when `haystack` contains `prefix` followed by a non-identifier
/// character (whole-token match). Unicode-aware: the character after the
/// prefix is checked with `is_alphanumeric`.
fn contains_token_prefix(haystack: &str, prefix: &str) -> bool {
let prefix_len = prefix.len();
haystack.match_indices(prefix).any(|(i, _)| {
haystack
.get(i + prefix_len..)
.and_then(|rest| rest.chars().next())
.is_none_or(|c| !c.is_alphanumeric() && c != '_')
})
}

/// Collect forbidden primitive type tokens from the return section (after `->`).
fn collect_python_return_primitives(lower: &str, forbidden: &mut Vec<&'static str>) {
let Some(arrow_idx) = lower.find("->") else {
return;
};
let ret = lower[arrow_idx + 2..].trim();
if ret.starts_with("str") {
// Match whole tokens only: `-> int` fires, `-> IntervalVO` does not.
if ret.starts_with("str") && is_token_end(ret, 3) {
forbidden.push("str");
}
if ret.starts_with("int") {
if ret.starts_with("int") && is_token_end(ret, 3) {
forbidden.push("int");
}
if ret.starts_with("float") {
if ret.starts_with("float") && is_token_end(ret, 5) {
forbidden.push("float");
}
// Only flag bare `list`/`dict` without type parameters
if ret.starts_with("list") && !ret.starts_with("list[") {
// Only flag bare `list`/`dict` without type parameters. Allow
// whitespace before the generic bracket (`list [ResultVO]`, `dict [K, V]`).
if ret.starts_with("list") && is_token_end(ret, 4) && !ret[4..].trim_start().starts_with('[') {
forbidden.push("list");
}
if ret.starts_with("dict") && !ret.starts_with("dict[") {
if ret.starts_with("dict") && is_token_end(ret, 4) && !ret[4..].trim_start().starts_with('[') {
forbidden.push("dict");
}
}

/// True when the text after `prefix_len` is not an identifier character,
/// so `prefix` stands alone as a whole token. Unicode-aware.
fn is_token_end(s: &str, prefix_len: usize) -> bool {
s.get(prefix_len..)
.and_then(|rest| rest.chars().next())
.is_none_or(|c| !c.is_alphanumeric() && c != '_')
}

/// Extract `(line_no, raw_signature_line)` for every method declaration inside a TypeScript
/// `interface` or `class` that uses primitive types in parameter/return annotations.
pub fn extract_typescript_method_signatures(content: &str) -> Vec<(usize, String)> {
Expand Down Expand Up @@ -300,7 +340,9 @@ fn push_inline_ts_signature(
fn brace_pair(line: &str) -> Option<(usize, usize)> {
let open = line.find('{')?;
let close = line.rfind('}')?;
Some((open, close))
// Guard against a reversed range (e.g. `} ... {` on one line) so the
// caller's `&trimmed[open + 1..close]` slice never panics.
(close > open).then_some((open, close))
}

/// True when the inline body of a one-line block uses primitive annotations.
Expand Down
45 changes: 38 additions & 7 deletions crates/shared/src/filesystem/taxonomy_filesystem_vo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,32 +593,63 @@ impl InboundLinkMap {
/// Priority 6: boundary-aligned suffix matching in both directions.
/// Requires the boundary to sit on a path separator so that e.g.
/// `foo_vo.rs` does not match `bar_vo.rs`.
/// Picks the longest (most specific) matching key; equal-length keys are
/// broken lexicographically so results are deterministic regardless of
/// HashMap iteration order. Keys are normalized the same way the matcher
/// normalizes them, so `/src/a.rs` and `src/a.rs` score identically.
fn boundary_suffix(&self, path: &str) -> Option<&Vec<String>> {
let clean = path.strip_prefix("./").unwrap_or(path);
let clean = normalize_path(path.strip_prefix("./").unwrap_or(path));
let mut best: Option<(&Vec<String>, usize, String)> = None;
for (k, v) in &self.mapping {
let k_clean = k.strip_prefix("./").unwrap_or(k);
if k_clean.is_empty() || clean.is_empty() {
let k_norm = normalize_path(k.strip_prefix("./").unwrap_or(k));
if k_norm.is_empty() || clean.is_empty() {
continue;
}
if boundary_ends_with(k_clean, clean) || boundary_ends_with(clean, k_clean) {
return Some(v);
let matched_len = if boundary_ends_with(k_norm, clean) {
Some(k_norm.len())
} else if boundary_ends_with(clean, k_norm) {
// Reverse direction: the matching key's length is what makes a
// candidate specific, so score by k_norm.len() here too.
Some(k_norm.len())
} else {
None
};
let Some(len) = matched_len else { continue };
let better = match best {
None => true,
Some((_, best_len, ref best_key)) => {
len > best_len || (len == best_len && k_norm < best_key.as_str())

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Boundary-suffix winner selection can still vary across runs when two mapping keys normalize to the same value. The equal-length tie-break only uses strict < on k_norm, so equal normalized keys keep whichever entry HashMap yields first; adding a secondary stable comparator (for example raw key) would keep this deterministic.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/shared/src/filesystem/taxonomy_filesystem_vo.rs, line 621:

<comment>Boundary-suffix winner selection can still vary across runs when two mapping keys normalize to the same value. The equal-length tie-break only uses strict `<` on `k_norm`, so equal normalized keys keep whichever entry HashMap yields first; adding a secondary stable comparator (for example raw key) would keep this deterministic.</comment>

<file context>
@@ -595,39 +595,45 @@ impl InboundLinkMap {
-                Some((_, best_len, best_key)) => {
-                    len > best_len || (len == best_len && k_clean < best_key)
+                Some((_, best_len, ref best_key)) => {
+                    len > best_len || (len == best_len && k_norm < best_key.as_str())
                 }
             };
</file context>
Fix with cubic

}
};
if better {
best = Some((v, len, k_norm.to_string()));
}
}
None
best.map(|(v, _, _)| v)
}
}

/// Strip leading `./` and `/` so equivalent path forms compare identically.
fn normalize_path(p: &str) -> &str {
p.trim_start_matches("./").trim_start_matches('/')

@cubic-dev-ai cubic-dev-ai Bot Aug 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Path normalization misses mixed prefixes like /./..., so equivalent keys do not always normalize identically before suffix scoring. That can skew "most specific" selection because k_norm.len() is used as the score.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/shared/src/filesystem/taxonomy_filesystem_vo.rs, line 634:

<comment>Path normalization misses mixed prefixes like `/./...`, so equivalent keys do not always normalize identically before suffix scoring. That can skew "most specific" selection because `k_norm.len()` is used as the score.</comment>

<file context>
@@ -595,39 +595,45 @@ impl InboundLinkMap {
 
+/// Strip leading `./` and `/` so equivalent path forms compare identically.
+fn normalize_path(p: &str) -> &str {
+    p.trim_start_matches("./").trim_start_matches('/')
+}
+
</file context>
Suggested change
p.trim_start_matches("./").trim_start_matches('/')
p.trim_start_matches('/').trim_start_matches("./").trim_start_matches('/')
Fix with cubic

}

/// True when `full` ends with `suffix` and the boundary before the suffix is
/// either the start of the string or a path separator (`/` or `\`).
/// Leading `./` and `/` are stripped from both sides first so a suffix like
/// `/b_vo.rs` is treated the same as `b_vo.rs` (the separator check then
/// applies to the byte just before the matched suffix).
fn boundary_ends_with(full: &str, suffix: &str) -> bool {
let full = full.trim_start_matches("./").trim_start_matches('/');
let suffix = suffix.trim_start_matches("./").trim_start_matches('/');
if !full.ends_with(suffix) {
return false;
}
let before = full.len() - suffix.len();
before == 0
|| full
.as_bytes()
.get(before)
.get(before - 1)
.is_some_and(|b| *b == b'/' || *b == b'\\')
}

Expand Down
22 changes: 22 additions & 0 deletions crates/shared/tests/unit_shared_common_vo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,28 @@ fn forbidden_primitive_detection_python() {
assert!(clean.is_empty());
}

#[test]
fn python_generic_brackets_with_space_not_flagged_as_bare_list_dict() {
// `list [ResultVO]` / `dict [KeyVO, ValueVO]` are parameterized and must
// not be reported as bare `list` / `dict`.
let found = python_signature_uses_forbidden_primitive("def run(self) -> list [ResultVO]:");
assert!(
!found.contains(&"list"),
"list [ResultVO] should not be bare list"
);

let found =
python_signature_uses_forbidden_primitive("def run(self) -> dict [KeyVO, ValueVO]:");
assert!(
!found.contains(&"dict"),
"dict [K, V] should not be bare dict"
);

// Bare list/dict without brackets are still flagged.
let found = python_signature_uses_forbidden_primitive("def run(self) -> list:");
assert!(found.contains(&"list"));
}

#[test]
fn forbidden_primitive_detection_typescript() {
let found = typescript_signature_uses_forbidden_primitive("getName(x: string): any");
Expand Down
10 changes: 10 additions & 0 deletions crates/shared/tests/unit_shared_filesystem_vo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,16 @@ fn get_importers_boundary_suffix_does_not_partial_match() {
assert!(map.get_importers("/b_vo.rs").is_none());
}

#[test]
fn get_importers_boundary_suffix_picks_most_specific_key() {
// When multiple keys suffix-match a query, the longest (most specific)
// key must win regardless of HashMap iteration order.
let map = map_of(&[("src/a.rs", &["specific.rs"]), ("a.rs", &["generic.rs"])]);
let result = map.get_importers("root/src/a.rs");
assert_eq!(result.map(|v| v.len()), Some(1));
assert_eq!(result.unwrap()[0], "specific.rs");
}

#[test]
fn get_importers_missing_returns_none() {
let map = map_of(&[("a.rs", &["b.rs"])]);
Expand Down
Loading