diff --git a/scripts/periphery-intersection.d.mts b/scripts/periphery-intersection.d.mts index 0a08225940c..e6ad860aa04 100644 --- a/scripts/periphery-intersection.d.mts +++ b/scripts/periphery-intersection.d.mts @@ -23,6 +23,10 @@ export function parseRepoLocation(location: string): { file: string; line: string; }; +export function filterIgnoredFindings( + findings: PeripheryFinding[], + repoRoot?: string, +): PeripheryFinding[]; export function escapeCommandData(value: unknown): string; export function escapeCommandProperty(value: unknown): string; export function formatAnnotation(finding: PeripheryFinding): string; diff --git a/scripts/periphery-intersection.mjs b/scripts/periphery-intersection.mjs index 92c8e16ae53..6a0fd9f5b49 100644 --- a/scripts/periphery-intersection.mjs +++ b/scripts/periphery-intersection.mjs @@ -5,6 +5,8 @@ import path from "node:path"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; const SHARED_LOCATION_PREFIX = "../shared/OpenClawKit/Sources/"; +const SHARED_SOURCE_ROOT = "apps/shared/OpenClawKit/Sources"; +const BARE_PERIPHERY_IGNORE_COMMENT = /\/\/\/?\s*periphery:ignore(?![:\w])/; function requireValue(args, index, option) { const value = args[index + 1]; @@ -107,6 +109,39 @@ export function parseRepoLocation(location) { }; } +export function filterIgnoredFindings(findings, repoRoot = process.cwd()) { + const sourceRoot = path.resolve(repoRoot, SHARED_SOURCE_ROOT); + const sourceLines = new Map(); + + return findings.filter((finding) => { + const location = parseRepoLocation(finding.location); + const sourceFile = path.resolve(repoRoot, location.file); + const relativeSource = path.relative(sourceRoot, sourceFile); + if ( + !relativeSource || + path.isAbsolute(relativeSource) || + relativeSource === ".." || + relativeSource.startsWith(`..${path.sep}`) + ) { + throw new Error(`invalid shared Periphery source path: ${location.file}`); + } + let lines = sourceLines.get(sourceFile); + if (!lines) { + lines = fs.readFileSync(sourceFile, "utf8").split(/\r?\n/); + sourceLines.set(sourceFile, lines); + } + + const declarationIndex = Number(location.line) - 1; + if (declarationIndex < 0 || declarationIndex >= lines.length) { + throw new Error(`invalid shared Periphery source line: ${finding.location}`); + } + + return ![lines[declarationIndex - 1], lines[declarationIndex]].some( + (line) => typeof line === "string" && BARE_PERIPHERY_IGNORE_COMMENT.test(line), + ); + }); +} + export function escapeCommandData(value) { return String(value ?? "") .replaceAll("%", "%25") @@ -166,9 +201,11 @@ export function run(args, env = process.env) { const options = parseArgs(args); readStatus(options.iosStatus, "iOS"); readStatus(options.macosStatus, "macOS"); - const findings = intersectFindings( - readFindings(options.iosResults, "iOS"), - readFindings(options.macosResults, "macOS"), + const findings = filterIgnoredFindings( + intersectFindings( + readFindings(options.iosResults, "iOS"), + readFindings(options.macosResults, "macOS"), + ), ); fs.mkdirSync(path.dirname(options.output), { recursive: true }); diff --git a/test/scripts/periphery-intersection.test.ts b/test/scripts/periphery-intersection.test.ts index 9d888734a3f..2573b69ea7b 100644 --- a/test/scripts/periphery-intersection.test.ts +++ b/test/scripts/periphery-intersection.test.ts @@ -1,9 +1,12 @@ -import { readFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; import { compileFunction } from "node:vm"; import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { buildSummary, + filterIgnoredFindings, formatAnnotation, intersectFindings, parseRepoLocation, @@ -11,6 +14,7 @@ import { } from "../../scripts/periphery-intersection.mjs"; const WORKFLOW_PATH = ".github/workflows/shared-openclawkit-periphery.yml"; +const FINDING_SOURCE = "../shared/OpenClawKit/Sources/OpenClawKit/Example.swift"; type WorkflowStep = { id?: string; @@ -35,12 +39,24 @@ function finding(overrides: Record = {}) { return { ids: ["s:11OpenClawKit7ExampleV"], kind: "struct", - location: "../shared/OpenClawKit/Sources/OpenClawKit/Example.swift:12:8", + location: `${FINDING_SOURCE}:12:8`, name: "Example", ...overrides, }; } +function withSharedSource(source: string, test: (repoRoot: string) => void) { + const repoRoot = mkdtempSync(join(tmpdir(), "openclaw-periphery-intersection-")); + const sourceFile = join(repoRoot, "apps/shared/OpenClawKit/Sources/OpenClawKit/Example.swift"); + mkdirSync(dirname(sourceFile), { recursive: true }); + writeFileSync(sourceFile, source); + try { + test(repoRoot); + } finally { + rmSync(repoRoot, { force: true, recursive: true }); + } +} + describe("Periphery intersection", () => { it("matches exact Swift USRs instead of declaration names", () => { const sameNameDifferentUsr = finding({ ids: ["s:11OpenClawKit7ExampleV_other"] }); @@ -62,6 +78,32 @@ describe("Periphery intersection", () => { expect(intersectFindings([later, finding()], [finding(), later])).toEqual([finding(), later]); }); + it("honors bare Periphery ignore comments on or above declarations", () => { + withSharedSource( + [ + "// periphery:ignore - exported package surface", + "public struct Example {}", + "", + 'public init(value: String = "value") {} // periphery:ignore - exported initializer', + ].join("\n"), + (repoRoot) => { + const declaration = finding({ location: `${FINDING_SOURCE}:2:8` }); + const inline = finding({ location: `${FINDING_SOURCE}:4:8` }); + expect(filterIgnoredFindings([declaration, inline], repoRoot)).toEqual([]); + }, + ); + }); + + it("does not treat scoped Periphery commands as bare ignores", () => { + withSharedSource( + ["// periphery:ignore:parameters value", "public struct Example {}"].join("\n"), + (repoRoot) => { + const command = finding({ location: `${FINDING_SOURCE}:2:8` }); + expect(filterIgnoredFindings([command], repoRoot)).toEqual([command]); + }, + ); + }); + it("fails closed when a finding has no USR", () => { expect(() => validateFindings([finding({ ids: [] })], "iOS")).toThrow( "iOS finding 0 has no usable Swift USR",