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
3 changes: 2 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion cli/build/drc-diagnostic-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ const EMPTY_IGNORE_COUNTS = (): DrcIgnoreCounts => ({
pin_specification: 0,
placement: 0,
routing: 0,
source: 0,
unknown: 0,
})

const normalizeCategory = (category: string): DrcCategory =>
category === "netlist" ||
category === "pin_specification" ||
category === "placement" ||
category === "routing"
category === "routing" ||
category === "source"
? category
: "unknown"

Expand Down Expand Up @@ -112,6 +114,7 @@ export const formatIgnoredDrcCounts = (counts: DrcIgnoreCounts): string =>
["pin_specification", counts.pin_specification],
["placement", counts.placement],
["routing", counts.routing],
["source", counts.source],
["unknown", counts.unknown],
] as const
)
Expand Down
1 change: 1 addition & 0 deletions cli/build/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ export const registerBuild = (program: Command) => {
pin_specification: 0,
placement: 0,
routing: 0,
source: 0,
unknown: 0,
}
const staticFileReferences: StaticBuildFileReference[] = []
Expand Down
26 changes: 26 additions & 0 deletions cli/check/register.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,33 @@
import type { AnyCircuitElement } from "circuit-json"
import type { Command } from "commander"
import {
analyzeCircuitJson,
formatCircuitJsonDiagnostics,
} from "lib/shared/circuit-json-diagnostics"
import { getCircuitJsonForCheck, resolveCheckInputFilePath } from "./shared"

export const check = async (file?: string) => {
const resolvedInputFilePath = await resolveCheckInputFilePath(file)
const circuitJson = (await getCircuitJsonForCheck({
filePath: resolvedInputFilePath,
platformConfig: {},
allowPrebuiltCircuitJson: true,
})) as AnyCircuitElement[]

return formatCircuitJsonDiagnostics(analyzeCircuitJson(circuitJson))
}

export const registerCheck = (program: Command) => {
program
.command("check")
.description("Partially build and validate circuit artifacts")
.argument("[file]", "Path to the entry file")
.action(async (file?: string) => {
try {
console.log(await check(file))
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
})
}
48 changes: 48 additions & 0 deletions cli/check/source/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { categorizeErrorOrWarning } from "@tscircuit/circuit-json-util"
import type { PlatformConfig } from "@tscircuit/props"
import type { AnyCircuitElement } from "circuit-json"
import type { Command } from "commander"
import {
type CircuitJsonIssue,
analyzeCircuitJson,
formatCircuitJsonDiagnostics,
} from "lib/shared/circuit-json-diagnostics"
import { getCircuitJsonForCheck, resolveCheckInputFilePath } from "../shared"

export const isSourceDiagnostic = (issue: CircuitJsonIssue) =>
categorizeErrorOrWarning(issue) === "source"

export const checkSource = async (file?: string) => {
const resolvedInputFilePath = await resolveCheckInputFilePath(file)
const circuitJson = (await getCircuitJsonForCheck({
filePath: resolvedInputFilePath,
platformConfig: {
pcbDisabled: true,
routingDisabled: true,
placementDrcChecksDisabled: true,
} satisfies PlatformConfig,
allowPrebuiltCircuitJson: true,
})) as AnyCircuitElement[]
const diagnostics = analyzeCircuitJson(circuitJson)

return formatCircuitJsonDiagnostics({
errors: diagnostics.errors.filter(isSourceDiagnostic),
warnings: diagnostics.warnings.filter(isSourceDiagnostic),
})
}

export const registerCheckSource = (program: Command) => {
program.commands
.find((command) => command.name() === "check")!
.command("source")
.description("Partially build and validate source diagnostics")
.argument("[file]", "Path to the entry file")
.action(async (file?: string) => {
try {
console.log(await checkSource(file))
} catch (error) {
console.error(error instanceof Error ? error.message : String(error))
process.exit(1)
}
})
}
2 changes: 2 additions & 0 deletions cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { registerCheck } from "./check/register"
import { registerCheckRoutingDifficulty } from "./check/routing-difficulty/register"
import { registerCheckSchematicPlacement } from "./check/schematic-placement/register"
import { registerCheckShorts } from "./check/shorts/register"
import { registerCheckSource } from "./check/source/register"
import { registerCheckTraceLength } from "./check/trace-length/register"
import { registerClone } from "./clone/register"
import { registerConfigPrint } from "./config/print/register"
Expand Down Expand Up @@ -89,6 +90,7 @@ registerCheckPlacement(program)
registerCheckRoutingDifficulty(program)
registerCheckSchematicPlacement(program)
registerCheckShorts(program)
registerCheckSource(program)
registerCheckTraceLength(program)

registerRegistry(program)
Expand Down
26 changes: 22 additions & 4 deletions lib/shared/circuit-json-diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,33 @@ export function analyzeCircuitJson(circuitJson: any[]): {
const isTypedError = typeof t === "string" && t.endsWith("_error")
const isTypedWarning = typeof t === "string" && t.endsWith("_warning")

if (hasErrorType || isTypedError) {
errors.push(item as CircuitJsonIssue)
if (hasWarningType || isTypedWarning) {
warnings.push(item as CircuitJsonIssue)
continue
}

if (hasWarningType || isTypedWarning) {
warnings.push(item as CircuitJsonIssue)
if (hasErrorType || isTypedError) {
errors.push(item as CircuitJsonIssue)
}
}

return { errors, warnings }
}

export function formatCircuitJsonDiagnostics({
errors,
warnings,
}: {
errors: CircuitJsonIssue[]
warnings: CircuitJsonIssue[]
}): string {
const lines = [`Errors: ${errors.length}`, `Warnings: ${warnings.length}`]

for (const issue of [...errors, ...warnings]) {
const issueType =
issue.warning_type ?? issue.error_type ?? issue.type ?? "unknown_issue"
lines.push(`- ${issueType}: ${issue.message ?? ""}`)
}

return lines.join("\n")
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"@tscircuit/circuit-json-placement-analysis": "^0.0.6",
"@tscircuit/circuit-json-routing-analysis": "^0.0.6",
"@tscircuit/circuit-json-schematic-placement-analysis": "github:tscircuit/circuit-json-schematic-placement-analysis#bb0b41d80c9714695b498e182c2a558f1e0ceb2d",
"@tscircuit/circuit-json-util": "^0.0.105",
"@tscircuit/eval": "^0.0.1016",
"@tscircuit/fake-snippets": "^0.0.182",
"@tscircuit/file-server": "^0.0.32",
Expand Down
16 changes: 16 additions & 0 deletions tests/analyze-circuit-json-warning-type-precedence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { expect, test } from "bun:test"
import { analyzeCircuitJson } from "lib/shared/circuit-json-diagnostics"

test("analyzeCircuitJson prefers a warning type over error_type metadata", () => {
const { errors, warnings } = analyzeCircuitJson([
{
type: "source_property_ignored_warning",
error_type: "source_property_ignored_warning",
property_name: "positiveConnection",
message: "ambiguous differential-pair trace",
},
])

expect(errors).toHaveLength(0)
expect(warnings).toHaveLength(1)
})
40 changes: 40 additions & 0 deletions tests/cli/check/check-all-diagnostics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { expect, test } from "bun:test"
import { writeFile } from "node:fs/promises"
import path from "node:path"
import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture"

test("check prints diagnostics from every category including unknown", async () => {
const { tmpDir, runCommand } = await getCliTestFixture()
const circuitJsonPath = path.join(tmpDir, "all-diagnostics.circuit.json")
const diagnosticTypes = [
"source_property_ignored_warning",
"source_pin_must_be_connected_error",
"pcb_component_outside_board_error",
"pcb_trace_error",
"source_no_power_pin_defined_warning",
"future_diagnostic_warning",
]

await writeFile(
circuitJsonPath,
JSON.stringify(
diagnosticTypes.map((type) => ({
type,
message: `Diagnostic for ${type}`,
})),
),
)

const { stdout, stderr, exitCode } = await runCommand(
`tsci check ${circuitJsonPath}`,
)

expect(exitCode).toBe(0)
expect(stderr).toBe("")
expect(stdout).toContain("Errors: 3")
expect(stdout).toContain("Warnings: 3")

for (const type of diagnosticTypes) {
expect(stdout).toContain(type)
}
})
42 changes: 42 additions & 0 deletions tests/cli/check/check-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { expect, test } from "bun:test"
import { writeFile } from "node:fs/promises"
import path from "node:path"
import { getCliTestFixture } from "../../fixtures/get-cli-test-fixture"

test("check source prints only source diagnostics", async () => {
const { tmpDir, runCommand } = await getCliTestFixture()
const circuitJsonPath = path.join(tmpDir, "source-warning.circuit.json")

await writeFile(
circuitJsonPath,
JSON.stringify([
{
type: "source_property_ignored_warning",
error_type: "source_property_ignored_warning",
message: "Source property was ignored",
},
{
type: "source_pin_must_be_connected_error",
error_type: "source_pin_must_be_connected_error",
message: "Pin must be connected",
},
{
type: "pcb_trace_error",
error_type: "pcb_trace_error",
message: "Trace failed",
},
]),
)

const { stdout, stderr, exitCode } = await runCommand(
`tsci check source ${circuitJsonPath}`,
)

expect(exitCode).toBe(0)
expect(stderr).toBe("")
expect(stdout).toContain("Errors: 0")
expect(stdout).toContain("Warnings: 1")
expect(stdout).toContain("source_property_ignored_warning")
expect(stdout).not.toContain("source_pin_must_be_connected_error")
expect(stdout).not.toContain("pcb_trace_error")
})
Loading