← Commit history

Fusion-reject fixes: empty pin/connect name -> pad number; per-symbol pin-uniqueness (merged libs); lint ERRORs for empty pin/connect + unresolved connect (2.2.1)

John Lauer ·46c72fe9e4 ·3mo ago ·parent a333e64
1 file changed +91−26
src/main.rs+91−26
@@ -527,23 +527,32 @@ fn pin_length_keyword(len: f64) -> &'static str { }  fn dedup_pin_names(pins: &[KicadPin]) -> Vec<String> {-    let mut counts: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();-    for pin in pins {-        *counts.entry(&pin.name).or_insert(0) += 1;+    // Effective EAGLE pin name: when the KiCad pin name is empty (common on+    // mechanical / 1-pin parts: `(number "1")(name "")`), fall back to the pad+    // NUMBER. Fusion REJECTS `<pin name="">` / `<connect pin="">`, so a name is+    // mandatory. This is the single source of truth for both the <pin> and the+    // <connect> emission, so the fallback fixes both at once.+    let eff: Vec<String> = pins.iter().enumerate().map(|(i, p)| {+        let n = p.name.trim();+        if !n.is_empty() {+            n.to_string()+        } else {+            let num = p.number.trim();+            if !num.is_empty() { num.to_string() } else { format!("P{}", i + 1) }+        }+    }).collect();+    let mut counts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+    for n in &eff {+        *counts.entry(n.clone()).or_insert(0) += 1;     }-    let mut seen: std::collections::HashMap<&str, u32> = std::collections::HashMap::new();-    pins.iter().map(|pin| {-        let total = counts[pin.name.as_str()];-        if total == 1 {-            pin.name.clone()+    let mut seen: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+    eff.iter().map(|name| {+        if counts[name] == 1 {+            name.clone()         } else {-            let n = seen.entry(&pin.name).or_insert(0);-            *n += 1;-            if *n == 1 {-                pin.name.clone()-            } else {-                format!("{}@{}", pin.name, n)-            }+            let c = seen.entry(name.clone()).or_insert(0);+            *c += 1;+            if *c == 1 { name.clone() } else { format!("{}@{}", name, c) }         }     }).collect() }@@ -1084,18 +1093,74 @@ pub(crate) fn lint_lbr(content: &str) -> LintResult {         errors.push(LintError { code: "device-no-package".into(), message: "Device has no package attribute — Fusion needs a package to show the component".into() });     } -    // Check for duplicate pin names (EAGLE requires unique)-    let pin_re = regex_lite::Regex::new(r#"<pin name="([^"]+)""#).unwrap();-    let mut pin_names = std::collections::HashMap::new();-    for cap in pin_re.captures_iter(content) {-        *pin_names.entry(cap[1].to_string()).or_insert(0u32) += 1;+    // Per-symbol pin analysis. Pin names must be NON-EMPTY (Fusion rejects+    // <pin name="">) and UNIQUE WITHIN a symbol. Across symbols, a repeat is+    // LEGAL — e.g. a merged multi-part lib where four 1-pin symbols each have a+    // pin "1" — so the uniqueness check is scoped to each <symbol> block, not+    // the whole file.+    let sym_block_re = regex_lite::Regex::new(r#"(?s)<symbol name="([^"]*)">(.*?)</symbol>"#).unwrap();+    let pin_name_re = regex_lite::Regex::new(r#"<pin name="([^"]*)""#).unwrap();+    let mut sym_pins: std::collections::HashMap<String, std::collections::HashSet<String>> =+        std::collections::HashMap::new();+    for sc in sym_block_re.captures_iter(content) {+        let sname = sc[1].to_string();+        let body = &sc[2];+        let mut within: std::collections::HashMap<String, u32> = std::collections::HashMap::new();+        let mut set = std::collections::HashSet::new();+        for pc in pin_name_re.captures_iter(body) {+            let pname = pc[1].to_string();+            if pname.trim().is_empty() {+                errors.push(LintError {+                    code: "empty-pin-name".into(),+                    message: format!("Symbol '{}' has a <pin name=\"\"> — Fusion rejects empty pin names (fall back to the pad number)", sname),+                });+                continue;+            }+            *within.entry(pname.clone()).or_insert(0) += 1;+            set.insert(pname);+        }+        for (name, count) in &within {+            if *count > 1 {+                errors.push(LintError {+                    code: "duplicate-pin-name".into(),+                    message: format!("Pin name '{}' appears {} times in symbol '{}' — EAGLE requires unique pin names within a symbol (use @suffix)", name, count, sname),+                });+            }+        }+        sym_pins.insert(sname, set);     }-    for (name, count) in &pin_names {-        if *count > 1 {-            errors.push(LintError {-                code: "duplicate-pin-name".into(),-                message: format!("Pin name '{}' appears {} times — EAGLE requires unique pin names (use @suffix for duplicates)", name, count),-            });++    // Connect integrity: no empty pin=, and every connect's pin must resolve to a+    // named <pin> in its gate's symbol. Resolve per-deviceset (gate names like+    // G$1 repeat across devicesets, so a global map would cross-wire them).+    let deviceset_re = regex_lite::Regex::new(r#"(?s)<deviceset\b.*?</deviceset>"#).unwrap();+    let gate_re = regex_lite::Regex::new(r#"<gate name="([^"]*)" symbol="([^"]*)""#).unwrap();+    let connect_full_re = regex_lite::Regex::new(r#"<connect gate="([^"]*)" pin="([^"]*)" pad="([^"]*)"/>"#).unwrap();+    for ds in deviceset_re.captures_iter(content) {+        let block = &ds[0];+        let mut gate_sym: std::collections::HashMap<String, String> = std::collections::HashMap::new();+        for g in gate_re.captures_iter(block) {+            gate_sym.insert(g[1].to_string(), g[2].to_string());+        }+        for c in connect_full_re.captures_iter(block) {+            let (gate, pin) = (c[1].to_string(), c[2].to_string());+            if pin.trim().is_empty() {+                errors.push(LintError {+                    code: "empty-connect-pin".into(),+                    message: "A <connect> has pin=\"\" — Fusion rejects empty connect pins (map to the gate's pin name)".into(),+                });+                continue;+            }+            if let Some(sym) = gate_sym.get(&gate) {+                if let Some(names) = sym_pins.get(sym) {+                    if !names.contains(&pin) {+                        errors.push(LintError {+                            code: "connect-pin-unresolved".into(),+                            message: format!("Connect pin '{}' (gate '{}') matches no <pin name> in symbol '{}' — Fusion cannot bind it", pin, gate, sym),+                        });+                    }+                }+            }         }     }