diff --git a/README.md b/README.md index 76c3ee8..f3fafbb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ and output an array of arrays for any issues found. | --- | --- | | [`checkConnectorAccessibleOrientation`](./lib/check-connector-accessible-orientation.ts) | Returns `pcb_accessibility_error` for connectors whose orientation makes them inaccessible. | | [`checkAllPinsInComponentAreUnderspecified`](./lib/check-all-pins-in-component-are-underspecified.ts) | Returns `source_component_pins_underspecified_warning` when every pin on a chip lacks pin attributes. | +| [`checkNoPowerPinDefined`](./lib/check-no-power-pin-defined.ts) | Returns `source_no_power_pin_defined_warning` when a chip has no pin with `requires_power=true`. | +| [`checkNoGroundPinDefined`](./lib/check-no-ground-pin-defined.ts) | Returns `source_no_ground_pin_defined_warning` when a chip has no pin with `requires_ground=true`. | | [`checkDifferentNetViaSpacing`](./lib/check-different-net-via-spacing.ts) | Returns `pcb_via_clearance_error` if vias on different nets are too close together. | | [`checkEachPcbPortConnectedToPcbTraces`](./lib/check-each-pcb-port-connected-to-pcb-trace.ts) | Returns `pcb_trace_error` if any `source_port` is not connected to its corresponding PCB traces. | | [`checkEachPcbTraceNonOverlapping`](./lib/check-each-pcb-trace-non-overlapping/check-each-pcb-trace-non-overlapping.ts) | Returns `pcb_trace_error` when `pcb_trace` segments overlap incompatible geometry on the same layer. | @@ -28,9 +30,10 @@ and output an array of arrays for any issues found. | Function | Description | | --- | --- | | [`runAllPlacementChecks`](./lib/run-all-checks.ts) | Runs all placement checks (`checkViasOffBoard`, `checkPcbComponentsOutOfBoard`, `checkPcbComponentOverlap`, and `checkConnectorAccessibleOrientation`). | -| [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs all netlist checks (e.g. `checkPinMustBeConnected`, `checkAllPinsInComponentAreUnderspecified`). | +| [`runAllNetlistChecks`](./lib/run-all-checks.ts) | Runs netlist connectivity checks (currently `checkPinMustBeConnected`). | +| [`runAllPinSpecificationChecks`](./lib/run-all-checks.ts) | Runs pin specification checks (e.g. `checkAllPinsInComponentAreUnderspecified`, `checkNoPowerPinDefined`, and `checkNoGroundPinDefined`). | | [`runAllRoutingChecks`](./lib/run-all-checks.ts) | Runs all routing checks currently enabled (`checkEachPcbPortConnectedToPcbTraces`, `checkSourceTracesHavePcbTraces`, `checkEachPcbTraceNonOverlapping`, same/different net via spacing, and `checkPcbTracesOutOfBoard`). | -| [`runAllChecks`](./lib/run-all-checks.ts) | Runs all placement, netlist, and routing checks and returns a combined list of errors. | +| [`runAllChecks`](./lib/run-all-checks.ts) | Runs placement, netlist, pin specification, and routing checks and returns a combined list of issues. | ## Implementation Details diff --git a/index.ts b/index.ts index e6f8f1d..d616603 100644 --- a/index.ts +++ b/index.ts @@ -10,9 +10,12 @@ export { checkPcbTracesOutOfBoard } from "./lib/check-trace-out-of-board/checkTr export { checkPcbComponentOverlap } from "./lib/check-pcb-components-overlap/checkPcbComponentOverlap" export { checkPinMustBeConnected } from "./lib/check-pin-must-be-connected" export { checkAllPinsInComponentAreUnderspecified } from "./lib/check-all-pins-in-component-are-underspecified" +export { checkNoPowerPinDefined } from "./lib/check-no-power-pin-defined" +export { checkNoGroundPinDefined } from "./lib/check-no-ground-pin-defined" export { runAllChecks, runAllNetlistChecks, + runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks, } from "./lib/run-all-checks" diff --git a/lib/check-all-pins-in-component-are-underspecified.ts b/lib/check-all-pins-in-component-are-underspecified.ts index 48e08c0..57e2f53 100644 --- a/lib/check-all-pins-in-component-are-underspecified.ts +++ b/lib/check-all-pins-in-component-are-underspecified.ts @@ -1,22 +1,13 @@ import { cju } from "@tscircuit/circuit-json-util" import type { AnyCircuitElement, + SourceComponentPinsUnderspecifiedWarning, SourcePinAttributes, SourcePort, SourceComponentBase, } from "circuit-json" import { source_pin_attributes } from "circuit-json" -type SourceComponentPinsUnderspecifiedWarning = { - type: "source_component_pins_underspecified_warning" - source_component_pins_underspecified_warning_id: string - warning_type: "source_component_pins_underspecified_warning" - message: string - source_component_id: string - source_port_ids: string[] - subcircuit_id?: string -} - const PIN_ATTRIBUTE_KEYS = Object.keys( source_pin_attributes.shape, ) as (keyof SourcePinAttributes)[] diff --git a/lib/check-no-ground-pin-defined.ts b/lib/check-no-ground-pin-defined.ts new file mode 100644 index 0000000..d57fb68 --- /dev/null +++ b/lib/check-no-ground-pin-defined.ts @@ -0,0 +1,54 @@ +import { cju } from "@tscircuit/circuit-json-util" +import type { + AnyCircuitElement, + SourceComponentBase, + SourceNoGroundPinDefinedWarning, + SourcePort, +} from "circuit-json" + +/** + * Check that each chip has at least one pin marked as requires_ground=true. + * Returns warnings for chips where no pin declares requires_ground. + */ +export function checkNoGroundPinDefined( + circuitJson: AnyCircuitElement[], +): SourceNoGroundPinDefinedWarning[] { + const warnings: SourceNoGroundPinDefinedWarning[] = [] + const db = cju(circuitJson) + + const sourceComponents = db.source_component.list() as SourceComponentBase[] + const sourcePorts = db.source_port.list() as SourcePort[] + + const portsByComponent = new Map() + for (const port of sourcePorts) { + if (!port.source_component_id) continue + const existing = portsByComponent.get(port.source_component_id) ?? [] + existing.push(port) + portsByComponent.set(port.source_component_id, existing) + } + + for (const component of sourceComponents) { + if (component.ftype !== "simple_chip") continue + + const componentPorts = + portsByComponent.get(component.source_component_id) ?? [] + if (componentPorts.length === 0) continue + + const hasRequiredGroundPin = componentPorts.some( + (port) => port.requires_ground === true, + ) + if (hasRequiredGroundPin) continue + + warnings.push({ + type: "source_no_ground_pin_defined_warning", + source_no_ground_pin_defined_warning_id: `source_no_ground_pin_defined_warning_${component.source_component_id}`, + warning_type: "source_no_ground_pin_defined_warning", + message: `${component.name} has no pin with requires_ground=true`, + source_component_id: component.source_component_id, + source_port_ids: componentPorts.map((port) => port.source_port_id), + subcircuit_id: componentPorts[0]?.subcircuit_id, + }) + } + + return warnings +} diff --git a/lib/check-no-power-pin-defined.ts b/lib/check-no-power-pin-defined.ts new file mode 100644 index 0000000..04e54ec --- /dev/null +++ b/lib/check-no-power-pin-defined.ts @@ -0,0 +1,54 @@ +import { cju } from "@tscircuit/circuit-json-util" +import type { + AnyCircuitElement, + SourceComponentBase, + SourceNoPowerPinDefinedWarning, + SourcePort, +} from "circuit-json" + +/** + * Check that each chip has at least one pin marked as requires_power=true. + * Returns warnings for chips where no pin declares requires_power. + */ +export function checkNoPowerPinDefined( + circuitJson: AnyCircuitElement[], +): SourceNoPowerPinDefinedWarning[] { + const warnings: SourceNoPowerPinDefinedWarning[] = [] + const db = cju(circuitJson) + + const sourceComponents = db.source_component.list() as SourceComponentBase[] + const sourcePorts = db.source_port.list() as SourcePort[] + + const portsByComponent = new Map() + for (const port of sourcePorts) { + if (!port.source_component_id) continue + const existing = portsByComponent.get(port.source_component_id) ?? [] + existing.push(port) + portsByComponent.set(port.source_component_id, existing) + } + + for (const component of sourceComponents) { + if (component.ftype !== "simple_chip") continue + + const componentPorts = + portsByComponent.get(component.source_component_id) ?? [] + if (componentPorts.length === 0) continue + + const hasRequiredPowerPin = componentPorts.some( + (port) => port.requires_power === true, + ) + if (hasRequiredPowerPin) continue + + warnings.push({ + type: "source_no_power_pin_defined_warning", + source_no_power_pin_defined_warning_id: `source_no_power_pin_defined_warning_${component.source_component_id}`, + warning_type: "source_no_power_pin_defined_warning", + message: `${component.name} has no pin with requires_power=true`, + source_component_id: component.source_component_id, + source_port_ids: componentPorts.map((port) => port.source_port_id), + subcircuit_id: componentPorts[0]?.subcircuit_id, + }) + } + + return warnings +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index 7e2898b..a1d0a14 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -8,6 +8,8 @@ import { checkViasOffBoard } from "./check-pcb-components-out-of-board/checkVias import { checkPcbComponentOverlap } from "./check-pcb-components-overlap/checkPcbComponentOverlap" import { checkConnectorAccessibleOrientation } from "./check-connector-accessible-orientation" import { checkPinMustBeConnected } from "./check-pin-must-be-connected" +import { checkNoGroundPinDefined } from "./check-no-ground-pin-defined" +import { checkNoPowerPinDefined } from "./check-no-power-pin-defined" import { checkSameNetViaSpacing } from "./check-same-net-via-spacing" import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-traces" import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceOutOfBoard" @@ -23,9 +25,16 @@ export async function runAllPlacementChecks(circuitJson: AnyCircuitElement[]) { } export async function runAllNetlistChecks(circuitJson: AnyCircuitElement[]) { + return [...checkPinMustBeConnected(circuitJson)] +} + +export async function runAllPinSpecificationChecks( + circuitJson: AnyCircuitElement[], +) { return [ - ...checkPinMustBeConnected(circuitJson), ...checkAllPinsInComponentAreUnderspecified(circuitJson), + ...checkNoPowerPinDefined(circuitJson), + ...checkNoGroundPinDefined(circuitJson), ] } @@ -45,6 +54,7 @@ export async function runAllChecks(circuitJson: AnyCircuitElement[]) { return [ ...(await runAllPlacementChecks(circuitJson)), ...(await runAllNetlistChecks(circuitJson)), + ...(await runAllPinSpecificationChecks(circuitJson)), ...(await runAllRoutingChecks(circuitJson)), ] } diff --git a/package.json b/package.json index 0af3cac..335907c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@types/debug": "^4.1.12", "bun-match-svg": "^0.0.11", "circuit-to-svg": "^0.0.333", - "circuit-json": "^0.0.392", + "circuit-json": "^0.0.395", "debug": "^4.3.5", "tscircuit": "^0.0.1439", "zod": "^3.23.8", diff --git a/tests/lib/check-no-ground-pin-defined.test.ts b/tests/lib/check-no-ground-pin-defined.test.ts new file mode 100644 index 0000000..24f6a7c --- /dev/null +++ b/tests/lib/check-no-ground-pin-defined.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkNoGroundPinDefined } from "lib/check-no-ground-pin-defined" + +describe("checkNoGroundPinDefined", () => { + test("returns warning when chip has no requires_ground pin", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "U1", + ftype: "simple_chip", + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "IO1", + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "IO2", + }, + ] + + const warnings = checkNoGroundPinDefined(circuitJson) + expect(warnings).toHaveLength(1) + expect(warnings[0].type).toBe("source_no_ground_pin_defined_warning") + expect(warnings[0].warning_type).toBe( + "source_no_ground_pin_defined_warning", + ) + expect(warnings[0].source_component_id).toBe("component_1") + }) + + test("returns no warning when at least one pin has requires_ground=true", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "U1", + ftype: "simple_chip", + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "GND", + requires_ground: true, + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "IO2", + }, + ] + + const warnings = checkNoGroundPinDefined(circuitJson) + expect(warnings).toHaveLength(0) + }) + + test("ignores non-chip components", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "R1", + ftype: "simple_resistor", + resistance: 1000, + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "pos", + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "neg", + }, + ] + + const warnings = checkNoGroundPinDefined(circuitJson) + expect(warnings).toHaveLength(0) + }) +}) diff --git a/tests/lib/check-no-power-pin-defined.test.ts b/tests/lib/check-no-power-pin-defined.test.ts new file mode 100644 index 0000000..de50abb --- /dev/null +++ b/tests/lib/check-no-power-pin-defined.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "bun:test" +import type { AnyCircuitElement } from "circuit-json" +import { checkNoPowerPinDefined } from "lib/check-no-power-pin-defined" + +describe("checkNoPowerPinDefined", () => { + test("returns warning when chip has no requires_power pin", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "U1", + ftype: "simple_chip", + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "IO1", + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "IO2", + }, + ] + + const warnings = checkNoPowerPinDefined(circuitJson) + expect(warnings).toHaveLength(1) + expect(warnings[0].type).toBe("source_no_power_pin_defined_warning") + expect(warnings[0].warning_type).toBe("source_no_power_pin_defined_warning") + expect(warnings[0].source_component_id).toBe("component_1") + }) + + test("returns no warning when at least one pin has requires_power=true", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "U1", + ftype: "simple_chip", + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "VCC", + requires_power: true, + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "IO2", + }, + ] + + const warnings = checkNoPowerPinDefined(circuitJson) + expect(warnings).toHaveLength(0) + }) + + test("ignores non-chip components", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_component", + source_component_id: "component_1", + name: "R1", + ftype: "simple_resistor", + resistance: 1000, + }, + { + type: "source_port", + source_port_id: "port_1", + source_component_id: "component_1", + name: "pos", + }, + { + type: "source_port", + source_port_id: "port_2", + source_component_id: "component_1", + name: "neg", + }, + ] + + const warnings = checkNoPowerPinDefined(circuitJson) + expect(warnings).toHaveLength(0) + }) +}) diff --git a/tests/lib/run-all-checks.test.ts b/tests/lib/run-all-checks.test.ts index 7f9b2dc..233a8d6 100644 --- a/tests/lib/run-all-checks.test.ts +++ b/tests/lib/run-all-checks.test.ts @@ -2,6 +2,7 @@ import { expect, test } from "bun:test" import { runAllChecks, runAllNetlistChecks, + runAllPinSpecificationChecks, runAllPlacementChecks, runAllRoutingChecks, } from "../.." // index.ts when imported from root @@ -82,7 +83,7 @@ test("runAllNetlistChecks excludes routing-only pcb trace connectivity checks", expect(netlistErrors).toEqual([]) expect(routingErrors.length).toBeGreaterThan(0) }) -test("runAllChecks equals placement + netlist + routing checks", async () => { +test("runAllChecks equals placement + netlist + pin specification + routing checks", async () => { const circuitJson: AnyCircuitElement[] = [ { type: "pcb_board", @@ -158,11 +159,13 @@ test("runAllChecks equals placement + netlist + routing checks", async () => { const allChecksErrors = await runAllChecks(circuitJson) const placementErrors = await runAllPlacementChecks(circuitJson) const netlistErrors = await runAllNetlistChecks(circuitJson) + const pinSpecificationErrors = await runAllPinSpecificationChecks(circuitJson) const routingErrors = await runAllRoutingChecks(circuitJson) expect(allChecksErrors).toEqual([ ...placementErrors, ...netlistErrors, + ...pinSpecificationErrors, ...routingErrors, ]) }) diff --git a/tests/lib/user-circuit-netlist.test.tsx b/tests/lib/user-circuit-netlist.test.tsx index 3716b5d..920cd69 100644 --- a/tests/lib/user-circuit-netlist.test.tsx +++ b/tests/lib/user-circuit-netlist.test.tsx @@ -65,6 +65,8 @@ test("test.tsx builds and has no netlist errors", async () => { name="USBC" pinAttributes={{ GND1: { mustBeConnected: false }, + VBUS1: { requiresPower: true }, + A4: { requiresPower: true }, }} connections={{ GND1: "net.GND", @@ -90,6 +92,7 @@ test("test.tsx builds and has no netlist errors", async () => { const circuitJson = circuit.getCircuitJson() - const netlistErrors = await runAllNetlistChecks(circuitJson as any) + const netlistIssues = await runAllNetlistChecks(circuitJson) + const netlistErrors = netlistIssues.filter((issue) => "error_type" in issue) expect(netlistErrors).toEqual([]) })