diff --git a/actions/parse-ci-reports/src/parsers/AstroCheckParser.js b/actions/parse-ci-reports/src/parsers/AstroCheckParser.js index 41af0c5..e550410 100644 --- a/actions/parse-ci-reports/src/parsers/AstroCheckParser.js +++ b/actions/parse-ci-reports/src/parsers/AstroCheckParser.js @@ -12,17 +12,25 @@ const ANSI_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"); * Parser for output generated by `astro check` */ export class AstroCheckParser extends BaseParser { - canParse(_filePath, content) { + canParse(filePath, content) { if (!content) { return false; } const sanitized = this.#stripAnsi(content); - if (LOCATION_PATTERN.test(sanitized)) { + const hasAstroMarkers = + LOCATION_PATTERN.test(sanitized) || + ASTRO_HEADER_PATTERNS.some((pattern) => pattern.test(sanitized)); + + if (hasAstroMarkers) { return true; } - return ASTRO_HEADER_PATTERNS.some((pattern) => pattern.test(sanitized)); + const normalizedPath = filePath.toLowerCase(); + const hasSupportedName = + this.matchesAutoPatterns(filePath) || normalizedPath.includes("astro"); + + return hasSupportedName; } getPriority() { diff --git a/actions/parse-ci-reports/src/parsers/AstroCheckParser.test.js b/actions/parse-ci-reports/src/parsers/AstroCheckParser.test.js index 82bfb2b..f86e2df 100644 --- a/actions/parse-ci-reports/src/parsers/AstroCheckParser.test.js +++ b/actions/parse-ci-reports/src/parsers/AstroCheckParser.test.js @@ -23,6 +23,14 @@ Result (26 files): - 0 hints`; describe("AstroCheckParser", () => { + it("keeps auto-pattern path detection synchronized", () => { + const parser = new AstroCheckParser(); + const filePath = "application/humanize-astro-check-report.log"; + + assert.ok(parser.matchesAutoPatterns(filePath)); + assert.ok(parser.canParse(filePath, SAMPLE_ERROR)); + }); + it("identifies astro check diagnostics", () => { const parser = new AstroCheckParser(); diff --git a/actions/parse-ci-reports/src/parsers/BaseParser.js b/actions/parse-ci-reports/src/parsers/BaseParser.js index a4b9d29..36005f9 100644 --- a/actions/parse-ci-reports/src/parsers/BaseParser.js +++ b/actions/parse-ci-reports/src/parsers/BaseParser.js @@ -12,6 +12,10 @@ export const ReportCategory = { * Follows the Strategy pattern for different report formats */ export class BaseParser { + constructor() { + this._autoPatternRegexCache = null; + } + /** * Build glob patterns for file basenames matched anywhere in the workspace. * @param {string[]} baseNames - File basenames to match @@ -66,6 +70,52 @@ export class BaseParser { return extensions.map((extension) => `**/*.${extension}`); } + /** + * Check if a file path matches this parser auto-detection patterns. + * @param {string} filePath - Path to evaluate + * @returns {boolean} True if path matches any auto pattern + */ + matchesAutoPatterns(filePath) { + if (!filePath) { + return false; + } + + const normalizedPath = this._normalizeFilePath(filePath).toLowerCase(); + const matchers = this._getAutoPatternRegexes(); + + return matchers.some((matcher) => matcher.test(normalizedPath)); + } + + _getAutoPatternRegexes() { + if (this._autoPatternRegexCache) { + return this._autoPatternRegexCache; + } + + const patterns = this.getAutoPatterns(); + this._autoPatternRegexCache = patterns.map((pattern) => + this._globToRegex(pattern), + ); + + return this._autoPatternRegexCache; + } + + _normalizeFilePath(filePath) { + return String(filePath).replace(/\\/g, "/"); + } + + _globToRegex(pattern) { + const escapedPattern = pattern + .toLowerCase() + .replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&") + .replace(/\*\*\//g, "<<>>") + .replace(/\*\*/g, "<<>>") + .replace(/\*/g, "[^/]*") + .replace(/<<>>/g, "(?:.*/)?") + .replace(/<<>>/g, ".*"); + + return new RegExp(`^${escapedPattern}$`, "i"); + } + /** * Parse a report file * @param {string} content - The file content diff --git a/actions/parse-ci-reports/src/parsers/CheckStyleParser.js b/actions/parse-ci-reports/src/parsers/CheckStyleParser.js index c4a482b..60992a5 100644 --- a/actions/parse-ci-reports/src/parsers/CheckStyleParser.js +++ b/actions/parse-ci-reports/src/parsers/CheckStyleParser.js @@ -16,12 +16,14 @@ export class CheckStyleParser extends BaseParser { } canParse(filePath, content) { - return ( - (filePath.toLowerCase().includes("checkstyle") || - filePath.endsWith(".xml")) && - content.includes(" { assert.strictEqual(parser.canParse("test-results.xml", content), true); }); + it("keeps auto-pattern path detection synchronized", () => { + const filePath = "reports/junit-report.xml"; + const content = ``; + + assert.ok(parser.matchesAutoPatterns(filePath)); + assert.ok(parser.canParse(filePath, content)); + }); + it("should parse simple JUnit XML", () => { const content = ` diff --git a/actions/parse-ci-reports/src/parsers/LCOVParser.js b/actions/parse-ci-reports/src/parsers/LCOVParser.js index 98022ee..9418270 100644 --- a/actions/parse-ci-reports/src/parsers/LCOVParser.js +++ b/actions/parse-ci-reports/src/parsers/LCOVParser.js @@ -7,8 +7,14 @@ import { ReportData, Coverage } from "../models/ReportData.js"; */ export class LCOVParser extends BaseParser { canParse(filePath, content) { + const normalizedPath = filePath.toLowerCase(); + const hasSupportedName = + this.matchesAutoPatterns(filePath) || + normalizedPath.includes("lcov") || + normalizedPath.endsWith(".info"); + return ( - (filePath.toLowerCase().includes("lcov") || filePath.endsWith(".info")) && + hasSupportedName && content.includes("TN:") && (content.includes("SF:") || content.includes("DA:")) ); diff --git a/actions/parse-ci-reports/src/parsers/PrettierParser.js b/actions/parse-ci-reports/src/parsers/PrettierParser.js index a7a9c6a..4a918f9 100644 --- a/actions/parse-ci-reports/src/parsers/PrettierParser.js +++ b/actions/parse-ci-reports/src/parsers/PrettierParser.js @@ -26,7 +26,15 @@ export class PrettierParser extends BaseParser { return true; } - if (filePath?.toLowerCase().includes("prettier")) { + const normalizedPath = filePath.toLowerCase(); + const hasSupportedName = + this.matchesAutoPatterns(filePath) || normalizedPath.includes("prettier"); + + if (!hasSupportedName) { + return false; + } + + if (normalizedPath.includes("prettier")) { return /\[(warn|error)\]\s+.+/i.test(content); } diff --git a/actions/parse-ci-reports/src/parsers/PrettierParser.test.js b/actions/parse-ci-reports/src/parsers/PrettierParser.test.js index a50d69e..24e3040 100644 --- a/actions/parse-ci-reports/src/parsers/PrettierParser.test.js +++ b/actions/parse-ci-reports/src/parsers/PrettierParser.test.js @@ -12,6 +12,14 @@ const SAMPLE_ERROR_LOG = `Checking formatting... [warn] Code style issues found in the above file(s). Forgot to run Prettier?`; describe("PrettierParser", () => { + it("keeps auto-pattern path detection synchronized", () => { + const parser = new PrettierParser(); + const filePath = "application/humanize-prettier-report.log"; + + assert.ok(parser.matchesAutoPatterns(filePath)); + assert.ok(parser.canParse(filePath, SAMPLE_FAILURE_LOG)); + }); + it("identifies prettier check logs", () => { const parser = new PrettierParser(); diff --git a/actions/parse-ci-reports/src/parsers/SarifParser.js b/actions/parse-ci-reports/src/parsers/SarifParser.js index 8395f1f..cf7bd94 100644 --- a/actions/parse-ci-reports/src/parsers/SarifParser.js +++ b/actions/parse-ci-reports/src/parsers/SarifParser.js @@ -6,11 +6,22 @@ import { ReportData, LintIssue } from "../models/ReportData.js"; * Parser for SARIF 2.1.0 static analysis results */ export class SarifParser extends BaseParser { - canParse(_filePath, content) { + canParse(filePath, content) { if (!content) { return false; } + const normalizedPath = filePath.toLowerCase(); + const hasSupportedName = + this.matchesAutoPatterns(filePath) || + normalizedPath.endsWith(".sarif") || + normalizedPath.endsWith(".sarif.json") || + normalizedPath.endsWith(".json"); + + if (!hasSupportedName) { + return false; + } + try { const sarif = JSON.parse(content); return ( diff --git a/actions/parse-ci-reports/src/parsers/SarifParser.test.js b/actions/parse-ci-reports/src/parsers/SarifParser.test.js index 34980b3..880d546 100644 --- a/actions/parse-ci-reports/src/parsers/SarifParser.test.js +++ b/actions/parse-ci-reports/src/parsers/SarifParser.test.js @@ -52,6 +52,14 @@ const SAMPLE_SARIF = JSON.stringify({ }); describe("SarifParser", () => { + it("keeps auto-pattern path detection synchronized", () => { + const parser = new SarifParser(); + const filePath = "reports/humanize-sarif-report.json"; + + assert.ok(parser.matchesAutoPatterns(filePath)); + assert.ok(parser.canParse(filePath, SAMPLE_SARIF)); + }); + it("identifies SARIF reports", () => { const parser = new SarifParser(); diff --git a/actions/parse-ci-reports/src/parsers/TAPParser.js b/actions/parse-ci-reports/src/parsers/TAPParser.js index e484c64..a63cec6 100644 --- a/actions/parse-ci-reports/src/parsers/TAPParser.js +++ b/actions/parse-ci-reports/src/parsers/TAPParser.js @@ -6,8 +6,16 @@ import { ReportData, TestResult } from "../models/ReportData.js"; * Supports TAP versions 12, 13, and 14 */ export class TAPParser extends BaseParser { - canParse(_filePath, content) { + canParse(filePath, content) { const lines = content.split("\n"); + const normalizedPath = filePath.toLowerCase(); + const hasSupportedName = + this.matchesAutoPatterns(filePath) || normalizedPath.endsWith(".tap"); + + if (!hasSupportedName) { + return false; + } + // TAP files typically start with TAP version or test plan return lines.some( (line) => diff --git a/actions/parse-ci-reports/src/parsers/TAPParser.test.js b/actions/parse-ci-reports/src/parsers/TAPParser.test.js index eb35c98..4b9ab88 100644 --- a/actions/parse-ci-reports/src/parsers/TAPParser.test.js +++ b/actions/parse-ci-reports/src/parsers/TAPParser.test.js @@ -15,6 +15,16 @@ ok 3 - test passed # SKIP`; assert.strictEqual(parser.canParse("test.tap", content), true); }); + it("keeps auto-pattern path detection synchronized", () => { + const filePath = "reports/results.tap"; + const content = `TAP version 13 +1..1 +ok 1 - sample`; + + assert.ok(parser.matchesAutoPatterns(filePath)); + assert.ok(parser.canParse(filePath, content)); + }); + it("should parse basic TAP output", () => { const content = `TAP version 13 1..3