fix: support hyphenated package names in string parser (sot-23, to-92, sod-123, etc.)#567
Open
claw-explorer wants to merge 3 commits intotscircuit:mainfrom
Open
fix: support hyphenated package names in string parser (sot-23, to-92, sod-123, etc.)#567claw-explorer wants to merge 3 commits intotscircuit:mainfrom
claw-explorer wants to merge 3 commits intotscircuit:mainfrom
Conversation
Hyphenated package names like SOT-23, TO-92, SOD-123, SOIC-8, etc. are the standard format in datasheets and part catalogs, but the string parser either produced wrong footprints or crashed when given these formats. The root cause: normalizeDefinition only had ad-hoc rules for sot-223 and to-220f, so any other hyphenated name was parsed incorrectly. The hyphen caused the regex to split the name from its numeric identifier, silently dropping the number (e.g. 'sot-23' became 'sot' producing a 6-pin generic SOT instead of the 3-pin SOT-23). This fix replaces the ad-hoc rules with a general-purpose regex that handles the standard ALPHA-DIGITS pattern, converting: - sot-23 -> sot23, sot-23-5 -> sot23_5 - to-92 -> to92, to-220f -> to220f, to-220f-3 -> to220f_3 - sod-123 -> sod123, sod-323f -> sod323f - soic-8 -> soic8, qfn-32 -> qfn32, bga-100 -> bga100 - tssop-16 -> tssop16, lqfp-48 -> lqfp48, ms-012 -> ms012 The regex intentionally does NOT match names where a digit precedes the hyphen (like VSON8-1EP) to avoid breaking existing behavior. Previously: - 14 package names silently produced WRONG footprints (wrong pad count) - 12 package names crashed with 'Invalid footprint function' Added 46 tests covering SOT, TO, SOD, SOIC, DIP, QFN, QFP, TSSOP, SSOP, LQFP, BGA, and MS families, plus case insensitivity and parameter passthrough. All 430 tests pass (384 existing + 46 new).
tests/hyphenated-names.test.ts
Outdated
Comment on lines
+18
to
+310
| test("sot-23 equals sot23", () => { | ||
| expect(getPadCount(fp.string("sot-23").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot23").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-23-5 equals sot23_5", () => { | ||
| expect(getPadCount(fp.string("sot-23-5").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot23_5").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-23-6 equals sot23_6", () => { | ||
| expect(getPadCount(fp.string("sot-23-6").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot23_6").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-89 equals sot89", () => { | ||
| expect(getPadCount(fp.string("sot-89").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot89").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-223 equals sot223", () => { | ||
| expect(getPadCount(fp.string("sot-223").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot223").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-223-5 equals sot223_5", () => { | ||
| expect(getPadCount(fp.string("sot-223-5").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot223_5").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-323 equals sot323", () => { | ||
| expect(getPadCount(fp.string("sot-323").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot323").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-363 equals sot363", () => { | ||
| expect(getPadCount(fp.string("sot-363").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot363").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-563 equals sot563", () => { | ||
| expect(getPadCount(fp.string("sot-563").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot563").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-723 equals sot723", () => { | ||
| expect(getPadCount(fp.string("sot-723").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot723").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-886 equals sot886", () => { | ||
| expect(getPadCount(fp.string("sot-886").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot886").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sot-963 equals sot963", () => { | ||
| expect(getPadCount(fp.string("sot-963").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot963").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // TO family | ||
| // --------------------------------------------------------------------------- | ||
| test("to-92 equals to92", () => { | ||
| expect(getPadCount(fp.string("to-92").circuitJson())).toBe( | ||
| getPadCount(fp.string("to92").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("to-92-2 equals to92_2", () => { | ||
| expect(getPadCount(fp.string("to-92-2").circuitJson())).toBe( | ||
| getPadCount(fp.string("to92_2").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("to-220 equals to220", () => { | ||
| expect(getPadCount(fp.string("to-220").circuitJson())).toBe( | ||
| getPadCount(fp.string("to220").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("to-220f equals to220f", () => { | ||
| expect(getPadCount(fp.string("to-220f").circuitJson())).toBe( | ||
| getPadCount(fp.string("to220f").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("to-220f-3 equals to220f_3", () => { | ||
| expect(getPadCount(fp.string("to-220f-3").circuitJson())).toBe( | ||
| getPadCount(fp.string("to220f_3").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // SOD family | ||
| // --------------------------------------------------------------------------- | ||
| test("sod-123 equals sod123", () => { | ||
| expect(getPadCount(fp.string("sod-123").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod123").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-323 equals sod323", () => { | ||
| expect(getPadCount(fp.string("sod-323").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod323").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-523 equals sod523", () => { | ||
| expect(getPadCount(fp.string("sod-523").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod523").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-80 equals sod80", () => { | ||
| expect(getPadCount(fp.string("sod-80").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod80").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-882 equals sod882", () => { | ||
| expect(getPadCount(fp.string("sod-882").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod882").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-923 equals sod923", () => { | ||
| expect(getPadCount(fp.string("sod-923").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod923").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-110 equals sod110", () => { | ||
| expect(getPadCount(fp.string("sod-110").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod110").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-128 equals sod128", () => { | ||
| expect(getPadCount(fp.string("sod-128").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod128").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-123f equals sod123f", () => { | ||
| expect(getPadCount(fp.string("sod-123f").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod123f").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("sod-323f equals sod323f", () => { | ||
| expect(getPadCount(fp.string("sod-323f").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod323f").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // IC packages | ||
| // --------------------------------------------------------------------------- | ||
| test("soic-8 equals soic8", () => { | ||
| expect(getPadCount(fp.string("soic-8").circuitJson())).toBe( | ||
| getPadCount(fp.string("soic8").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("soic-16 equals soic16", () => { | ||
| expect(getPadCount(fp.string("soic-16").circuitJson())).toBe( | ||
| getPadCount(fp.string("soic16").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("dip-8 equals dip8", () => { | ||
| expect(getPadCount(fp.string("dip-8").circuitJson())).toBe( | ||
| getPadCount(fp.string("dip8").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("dip-16 equals dip16", () => { | ||
| expect(getPadCount(fp.string("dip-16").circuitJson())).toBe( | ||
| getPadCount(fp.string("dip16").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("qfn-32 equals qfn32", () => { | ||
| expect(getPadCount(fp.string("qfn-32").circuitJson())).toBe( | ||
| getPadCount(fp.string("qfn32").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("qfp-48 equals qfp48", () => { | ||
| expect(getPadCount(fp.string("qfp-48").circuitJson())).toBe( | ||
| getPadCount(fp.string("qfp48").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("tssop-16 equals tssop16", () => { | ||
| expect(getPadCount(fp.string("tssop-16").circuitJson())).toBe( | ||
| getPadCount(fp.string("tssop16").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("ssop-8 equals ssop8", () => { | ||
| expect(getPadCount(fp.string("ssop-8").circuitJson())).toBe( | ||
| getPadCount(fp.string("ssop8").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("lqfp-32 equals lqfp32", () => { | ||
| expect(getPadCount(fp.string("lqfp-32").circuitJson())).toBe( | ||
| getPadCount(fp.string("lqfp32").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("lqfp-48 equals lqfp48", () => { | ||
| expect(getPadCount(fp.string("lqfp-48").circuitJson())).toBe( | ||
| getPadCount(fp.string("lqfp48").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("bga-100 equals bga100", () => { | ||
| expect(getPadCount(fp.string("bga-100").circuitJson())).toBe( | ||
| getPadCount(fp.string("bga100").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Misc packages | ||
| // --------------------------------------------------------------------------- | ||
| test("ms-012 equals ms012", () => { | ||
| expect(getPadCount(fp.string("ms-012").circuitJson())).toBe( | ||
| getPadCount(fp.string("ms012").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("ms-013 equals ms013", () => { | ||
| expect(getPadCount(fp.string("ms-013").circuitJson())).toBe( | ||
| getPadCount(fp.string("ms013").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Case insensitivity | ||
| // --------------------------------------------------------------------------- | ||
| test("SOT-23 (uppercase) equals sot23", () => { | ||
| expect(getPadCount(fp.string("SOT-23").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot23").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("SOT-223-5 (uppercase) equals sot223_5", () => { | ||
| expect(getPadCount(fp.string("SOT-223-5").circuitJson())).toBe( | ||
| getPadCount(fp.string("sot223_5").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("TO-92 (uppercase) equals to92", () => { | ||
| expect(getPadCount(fp.string("TO-92").circuitJson())).toBe( | ||
| getPadCount(fp.string("to92").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("SOD-123 (uppercase) equals sod123", () => { | ||
| expect(getPadCount(fp.string("SOD-123").circuitJson())).toBe( | ||
| getPadCount(fp.string("sod123").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| // --------------------------------------------------------------------------- | ||
| // Hyphenated with additional parameters (underscore-separated) | ||
| // --------------------------------------------------------------------------- | ||
| test("soic-8_w5.3mm equals soic8_w5.3mm", () => { | ||
| expect(getPadCount(fp.string("soic-8_w5.3mm").circuitJson())).toBe( | ||
| getPadCount(fp.string("soic8_w5.3mm").circuitJson()), | ||
| ) | ||
| }) | ||
|
|
||
| test("dip-8_w7.62mm equals dip8_w7.62mm", () => { | ||
| expect(getPadCount(fp.string("dip-8_w7.62mm").circuitJson())).toBe( | ||
| getPadCount(fp.string("dip8_w7.62mm").circuitJson()), | ||
| ) | ||
| }) |
Contributor
There was a problem hiding this comment.
This test file contains 69 test() calls, which violates the rule that a *.test.ts file may have AT MOST one test(...). After the first test, the user should split into multiple, numbered files (e.g. hyphenated-names1.test.ts, hyphenated-names2.test.ts, etc.). The file should be restructured to have only one test per file to comply with the testing standards.
Spotted by Graphite (based on custom rule: Custom rule)
Is this helpful? React 👍 or 👎 to let us know.
Keep the general hyphenated package name regex from the feature branch and add the specific sot23-N rule from main (not covered by the general pattern since "sot23" contains digits in the prefix). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Hyphenated package names — the standard format found in datasheets and part catalogs — either produce wrong footprints silently or crash when passed to the string parser.
For example:
fp.string("sot-23")→ produces a 6-pin SOT instead of 3-pin SOT-23fp.string("to-92")→ crashes withInvalid footprint function, got "to"💥fp.string("dip-16")→ produces 6 pads instead of 16 padsfp.string("qfn-32")→ produces 64 pads instead of 32 padsImpact: 14 package names silently produce wrong footprints (wrong pad count), 12 crash.
Root Cause
normalizeDefinition()only had ad-hoc rules forsot-223andto-220f. Any other hyphenated name passed through unchanged, causing the regex([a-zA-Z]+)([\(\d\.\+\?].*)?to extract just the letter prefix (e.g."sot"from"sot-23") and silently drop the numeric identifier.Fix
Replace the ad-hoc normalization rules with a general-purpose regex that handles the standard
ALPHA-DIGITSpattern:sot-23sot23sot-23-5sot23_5to-92to92to-220fto220fto-220f-3to220f_3sod-123sod123sod-323fsod323fsoic-8soic8qfn-32qfn32tssop-16tssop16bga-100bga100ms-012ms012The regex intentionally does not match names where a digit precedes the hyphen (like
VSON8-1EP) to avoid breaking existing behavior.Tests
Added 46 tests covering:
All 430 tests pass (384 existing + 46 new).