Skip to content

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
claw-explorer:fix/hyphenated-package-names
Open

fix: support hyphenated package names in string parser (sot-23, to-92, sod-123, etc.)#567
claw-explorer wants to merge 3 commits intotscircuit:mainfrom
claw-explorer:fix/hyphenated-package-names

Conversation

@claw-explorer
Copy link
Copy Markdown

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-23 ⚠️
  • fp.string("to-92") → crashes with Invalid footprint function, got "to" 💥
  • fp.string("dip-16") → produces 6 pads instead of 16 pads ⚠️
  • fp.string("qfn-32") → produces 64 pads instead of 32 pads ⚠️

Impact: 14 package names silently produce wrong footprints (wrong pad count), 12 crash.

Root Cause

normalizeDefinition() only had ad-hoc rules for sot-223 and to-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-DIGITS pattern:

Input Output
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
tssop-16 tssop16
bga-100 bga100
ms-012 ms012

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

  • SOT family (sot-23, sot-23-5, sot-89, sot-223-5, sot-323, sot-363, sot-563, sot-723, sot-886, sot-963)
  • TO family (to-92, to-92-2, to-220, to-220f, to-220f-3)
  • SOD family (sod-80, sod-110, sod-123, sod-123f, sod-128, sod-323, sod-323f, sod-523, sod-882, sod-923)
  • IC packages (soic-8/16, dip-8/16, qfn-32, qfp-48, tssop-16, ssop-8, lqfp-32/48, bga-100)
  • Misc (ms-012, ms-013)
  • Case insensitivity (SOT-23, SOT-223-5, TO-92, SOD-123)
  • Parameter passthrough (soic-8_w5.3mm, dip-8_w7.62mm)

All 430 tests pass (384 existing + 46 new).

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).
@claw-explorer claw-explorer requested a review from seveibar as a code owner April 1, 2026 14:15
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()),
)
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

claw-explorer and others added 2 commits April 1, 2026 15:11
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant