Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 | 17x 17x 25x 6x 131x 128x 181x 6x 131x 128x 181x 36x 131x 5x 6x 6x 4x 4x 131x 131x 131x 19x 6x 131x 131x 181x 128x 14x 7x 7x 128x 128x 128x 7x 131x 131x 131x 128x 89x 131x 5x 2x 131x 131x 131x 62x 24x 6x 6x 6x 18x 3x 3x 131x 3x 2x 1x 1x 131x 131x 131x 128x 148x 181x 128x 2x 2x 131x | import type { ParserMessage } from "../schema";
import { randomUUID } from "node:crypto";
import {
type StaticIOAnalysis,
type StaticIOCall,
type StaticIOOperation,
type UnresolvedStaticIOCall,
} from "@shared/io-registry-parser";
import { UNO_PWM_PINS, isUnoPwmPin } from "@shared/utils/arduino-utils";
const DIGITAL_OPERATIONS = new Set<StaticIOOperation>([
"digitalRead",
"digitalWrite",
]);
const LITERAL_PIN_EXPRESSION = /^(?:\d+|A\d+)$/;
function sourceLocation(call: StaticIOCall | UnresolvedStaticIOCall): Pick<ParserMessage, "file" | "line"> {
return {
line: call.line,
...(call.file ? { file: call.file } : {}),
};
}
function pinLabel(pinId: number): string {
return pinId >= 14 ? `A${pinId - 14}` : String(pinId);
}
function resolvedCalls(
analysis: StaticIOAnalysis,
operation: StaticIOOperation,
): StaticIOCall[] {
return analysis.pins
.flatMap(({ calls }) => calls)
.filter(({ op }) => op === operation)
.sort((a, b) => a.line - b.line);
}
function digitalCalls(analysis: StaticIOAnalysis): StaticIOCall[] {
return analysis.pins
.flatMap(({ calls }) => calls)
.filter(({ op }) => DIGITAL_OPERATIONS.has(op))
.sort((a, b) => a.line - b.line);
}
function unresolvedDigitalCalls(
analysis: StaticIOAnalysis,
): UnresolvedStaticIOCall[] {
return analysis.unresolvedCalls.filter(({ op }) =>
DIGITAL_OPERATIONS.has(op),
);
}
function createLiteralMissingPinModeMessage(call: StaticIOCall): ParserMessage {
const pin = call.sourceExpression;
return {
id: randomUUID(),
type: "warning",
category: "hardware",
severity: 2,
message: `Pin ${pin} used with digitalRead/digitalWrite but pinMode() was not called for this pin.`,
suggestion: `pinMode(${pin}, INPUT);`,
...sourceLocation(call),
};
}
function createVariableMissingPinModeMessage(
call: StaticIOCall | UnresolvedStaticIOCall,
): ParserMessage {
const variable = call.sourceExpression;
return {
id: randomUUID(),
type: "warning",
category: "hardware",
severity: 2,
message: `Variable '${variable}' used in digitalRead/digitalWrite but no pinMode() call found for this variable.`,
suggestion: `pinMode(${variable}, INPUT);`,
...sourceLocation(call),
};
}
/** Derives hardware compatibility messages from canonical static I/O facts. */
export class HardwareCompatibilityParser {
constructor(private readonly analysis: StaticIOAnalysis) {}
parse(): ParserMessage[] {
return [
...this.checkAnalogWritePWM(),
...this.checkPinModeConflicts(),
...this.checkMissingPinModes(),
...this.checkOutputPinsReadAsInput(),
];
}
private checkAnalogWritePWM(): ParserMessage[] {
return resolvedCalls(this.analysis, "analogWrite")
.filter(({ pinId }) => !isUnoPwmPin(pinId))
.map((call) => ({
id: randomUUID(),
type: "warning" as const,
category: "hardware" as const,
severity: 2 as const,
message: `analogWrite(${call.sourceExpression}, ...) used on pin ${call.pinId}, which doesn't support PWM on Arduino UNO. PWM pins: ${UNO_PWM_PINS.join(", ")}.`,
suggestion: "// Use PWM pin instead: analogWrite(3, value);",
...sourceLocation(call),
}));
}
private checkPinModeConflicts(): ParserMessage[] {
const messages: ParserMessage[] = [];
for (const { pinId, calls } of this.analysis.pins) {
const pinModeCalls = calls.filter(({ op }) => op === "pinMode");
if (pinModeCalls.length < 2) continue;
const modes = pinModeCalls.flatMap(({ mode }) => mode ?? []);
const uniqueModes = [...new Set(modes)];
const firstExpression = pinModeCalls[0]?.sourceExpression;
const pin =
firstExpression && LITERAL_PIN_EXPRESSION.test(firstExpression)
? firstExpression
: pinLabel(pinId);
const conflictCall = pinModeCalls[1];
if (uniqueModes.length > 1) {
messages.push({
id: randomUUID(),
type: "warning",
category: "pins",
severity: 2,
message: `Pin ${pin} has multiple pinMode() calls with different modes: ${uniqueModes.join(", ")}.`,
suggestion: `Use a single pinMode(${pin}, <MODE>) call in setup().`,
...(conflictCall ? sourceLocation(conflictCall) : {}),
});
} else E{
messages.push({
id: randomUUID(),
type: "warning",
category: "pins",
severity: 2,
message: `Pin ${pin} has pinMode() called multiple times (${pinModeCalls.length}x).`,
suggestion: `Remove duplicate pinMode(${pin}, ${uniqueModes[0]}) calls.`,
...(conflictCall ? sourceLocation(conflictCall) : {}),
});
}
}
return messages;
}
private checkMissingPinModes(): ParserMessage[] {
const messages: ParserMessage[] = [];
const configuredPins = new Set(
this.analysis.pins
.filter(({ calls }) => calls.some(({ op }) => op === "pinMode"))
.map(({ pinId }) => pinId),
);
const configuredExpressions = new Set(
this.analysis.unresolvedCalls
.filter(({ op }) => op === "pinMode")
.map(({ sourceExpression }) => sourceExpression),
);
const warnedLiterals = new Set<string>();
let warnedVariable = false;
for (const call of digitalCalls(this.analysis)) {
if (configuredPins.has(call.pinId)) continue;
if (LITERAL_PIN_EXPRESSION.test(call.sourceExpression)) {
Iif (warnedLiterals.has(call.sourceExpression)) continue;
warnedLiterals.add(call.sourceExpression);
messages.push(createLiteralMissingPinModeMessage(call));
} else if (!warnedVariable) {
messages.push(createVariableMissingPinModeMessage(call));
warnedVariable = true;
}
}
for (const call of unresolvedDigitalCalls(this.analysis)) {
if (warnedVariable || configuredExpressions.has(call.sourceExpression)) {
continue;
}
messages.push(createVariableMissingPinModeMessage(call));
warnedVariable = true;
}
return messages;
}
private checkOutputPinsReadAsInput(): ParserMessage[] {
const messages: ParserMessage[] = [];
for (const { pinId, calls } of this.analysis.pins) {
const isOutput = calls.some(
({ op, mode }) => op === "pinMode" && mode === "OUTPUT",
);
const readCall = calls.find(({ op }) => op === "digitalRead");
if (!isOutput || !readCall) continue;
const pin = pinLabel(pinId);
messages.push({
id: randomUUID(),
type: "warning",
category: "pins",
severity: 2,
message: `Pin ${pin} is configured as OUTPUT but read with digitalRead(). Reading an OUTPUT pin may return unexpected values.`,
suggestion: `If you need to read the pin, use pinMode(${pin}, INPUT) or INPUT_PULLUP instead.`,
...sourceLocation(readCall),
});
}
return messages;
}
}
|