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
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

71 changes: 58 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,91 @@ 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 (`[`, allowing whitespace between) —
/// 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
};
// Whole-token boundary AND not a parameterized generic type
// (`list [ResultVO]` / `list[ResultVO]` are not bare).
!next.is_alphanumeric() && next != '_' && !rest.trim_start().starts_with('[')
})
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 +343,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
38 changes: 38 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,44 @@ 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"
);

// Parameter-side spaced generic annotations are also not bare.
let found =
python_signature_uses_forbidden_primitive("def run(self, items: list [ResultVO]) -> bool:");
assert!(
!found.contains(&"list"),
"param list [ResultVO] should not be bare list"
);

let found = python_signature_uses_forbidden_primitive(
"def run(self, mapping: dict [KeyVO, ValueVO]) -> bool:",
);
assert!(
!found.contains(&"dict"),
"param 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