All files / shared/parsers pin-conflicts-parser.ts

90.9% Statements 20/22
74.07% Branches 20/27
100% Functions 13/13
100% Lines 17/17

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                      4x 4x 4x 4x   4x 4x 4x 4x       21x                       118x     118x 47x     95x     95x       47x           21x 21x                            
import type { ParserMessage } from "../schema";
import { randomUUID } from "node:crypto";
import {
  type StaticIOAnalysis,
  type StaticIOCall,
} from "@shared/io-registry-parser";
 
function compareDigitalCallOrder(
  a: { digitalCalls: StaticIOCall[] },
  b: { digitalCalls: StaticIOCall[] },
): number {
  const aPinModes = a.digitalCalls.filter(({ op }) => op === "pinMode");
  const bPinModes = b.digitalCalls.filter(({ op }) => op === "pinMode");
  Iif (aPinModes.length > 0 && bPinModes.length === 0) return -1;
  Iif (aPinModes.length === 0 && bPinModes.length > 0) return 1;
 
  const aCalls = aPinModes.length > 0 ? aPinModes : a.digitalCalls;
  const bCalls = bPinModes.length > 0 ? bPinModes : b.digitalCalls;
  return Math.min(...aCalls.map(({ line }) => line)) -
    Math.min(...bCalls.map(({ line }) => line));
}
 
function pinLabel(pinId: number): string {
  return pinId >= 14 ? `A${pinId - 14}` : String(pinId);
}
 
/**
 * Parser for pin conflicts (same pin used as digital and analog)
 * 
 * Rules:
 * 1. Digital + analog on same pin
 * 2. Multiple conflicts
 * 3. Numeric pin notation
 */
export class PinConflictsParser {
  constructor(private readonly analysis: StaticIOAnalysis) {}
 
  parse(): ParserMessage[] {
    return this.analysis.pins
      .map(({ pinId, calls }) => ({
        pinId,
        digitalCalls: calls.filter(({ op }) =>
          op === "pinMode" || op === "digitalRead" || op === "digitalWrite"
        ),
        analogCalls: calls.filter(({ op }) =>
          op === "analogRead" || op === "analogWrite"
        ),
      }))
      .filter(({ digitalCalls, analogCalls }) =>
        digitalCalls.length > 0 && analogCalls.length > 0
      )
      .sort(compareDigitalCallOrder)
      .map(({ pinId, digitalCalls, analogCalls }) => {
        // Project-aware calls carry their original file and line. Legacy
        // string analysis omits these additive fields for compatibility.
        const location = digitalCalls[0] ?? analogCalls[0];
        return {
          ...(location
            ? { line: location.line, ...(location.file ? { file: location.file } : {}) }
            : {}),
          id: randomUUID(),
          type: "warning" as const,
          category: "hardware" as const,
          severity: 2 as const,
          message: `Pin ${pinLabel(pinId)} used as both digital and analog. This may be unintended.`,
          suggestion: "// Use separate pins for digital and analog",
        };
      });
  }
}