diff --git a/portfolio/plugins.json b/portfolio/plugins.json index a439787..9a59049 100644 --- a/portfolio/plugins.json +++ b/portfolio/plugins.json @@ -9,10 +9,10 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "2a86fefc11dacc40abccb5d81fa181969ed249d56271c33832a7d271421342ff" }, - { "path": "composer.lock", "sha256": "24418414a9cbb488fe4cb760ef1404bd2ef5a97ea48db6ab1dcfa8752a0da7d2" }, + { "path": "composer.json", "sha256": "a0f05b2ae1fdd5a59a636d8494addfd2fbb3e0c1ffac86be65345e3490a3c2d4" }, + { "path": "composer.lock", "sha256": "daeb71dd77a6b35edb1ce2932303afb8d6ba94d300f111c4e0958940c6146785" }, { "path": "phpunit.xml.dist", "sha256": "5f91d0a818fe95020feccdeaa2bedf676297657e91b096ab96b97432375c5366" }, - { "path": "tests/bootstrap.php", "sha256": "c4798bd43326fe3bac514498e307636e87c9caad1311b27442710214547704db" } + { "path": "tests/bootstrap.php", "sha256": "0d84571462a1881329dcaf1b154f101677cb42143169b27394481afb4dcac73e" } ] } }, @@ -44,8 +44,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "f323bf5c9daffa7547f820f5fcf5646a98c23d0ca2a3a6c9c124d6b5c6375e6f" }, - { "path": "composer.lock", "sha256": "234ed8efc8d004e1a2d1bd2ffa52cc6059ad9e4bbf2c852e41e6d9ded05ddc33" }, + { "path": "composer.json", "sha256": "f3edff09db4f93818a96be4bf6223e6f9343c5a4b5e82e36dbc20ec813596552" }, + { "path": "composer.lock", "sha256": "dccc5e9541103ebffd188df27567f0172507ff0659b940821f4fffdecf138638" }, { "path": "phpunit.xml.dist", "sha256": "1e9f6232d0f55f82fc4211c1689c3a8db80cc1f8aa7959eca9a2c0b0b4d65327" }, { "path": "tests/bootstrap.php", "sha256": "e4403cfe2f517b3788879831c55536bfc471abd9b616afa61899d9ebf1875b8b" } ] @@ -85,7 +85,7 @@ { "path": "composer.json", "sha256": "74782b1130ca176259626ce18439f5d54730f8b583ab7d35069123949252110c" }, { "path": "composer.lock", "sha256": "553319c0d0f10b2765c7beb0af198bf9a1dbe1ad902439d14e1d74132ee4eab7" }, { "path": "phpunit.xml.dist", "sha256": "a51a3fb9d0a54d88f2f3d1ab04dda9375cf6700966f1b8f2b4fd0715936365c7" }, - { "path": "tests/bootstrap.php", "sha256": "5d46013ecc675591f97089db55e679a3eb434387b436ecaca2ac92dc5eac9223" } + { "path": "tests/bootstrap.php", "sha256": "cdc13526cbf8024198363bf37d66d21784794eaa2c780dc4dd2ccdda131c5510" } ] } }, @@ -152,8 +152,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "b731fd784932b9c2ed239e1aef5fb4cdcd01f9e6c949b7a2f1dfe416ab604152" }, - { "path": "composer.lock", "sha256": "4c18ed167cc85e1e6eb0e8c5b90f9ed66095372e3ba7f396a38a8c35e914ff3b" }, + { "path": "composer.json", "sha256": "70ee76ad4ac3489dc27af587bda1c76182710414d6636ce840f5e7e71a400d67" }, + { "path": "composer.lock", "sha256": "c6d7269d957ccea3236c319c97b63ab59d7b00215e681ed6fed27dbfa2d8874a" }, { "path": "phpunit.xml.dist", "sha256": "b3d7ea362089a835aedb77e47f647d413f06f020578aefa2fc009836530e43ba" }, { "path": "tests/bootstrap.php", "sha256": "8915f7184852b960c0eabcd2c7883c2cb7dc15193b530bb17720af62ebd507ea" } ] @@ -242,8 +242,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "cde20dfacd5021c0e5fff0c46b39d93ab6aafe283f8f1f2567a6ea8cd7f5605e" }, - { "path": "composer.lock", "sha256": "16d1fec986461ce188b64ebbf91271781f5356b60497920116d5c5186100c6cc" }, + { "path": "composer.json", "sha256": "ce9a18b73bfbbe056f52c13e0988303396fe3e1f5fa0088c1c3d99bb4f520b16" }, + { "path": "composer.lock", "sha256": "04db6d314a1a4bde8af9e8cca4b446cb295b3f87d60ac4aab942c270f30d56a7" }, { "path": "phpunit.xml.dist", "sha256": "93980825969bc906cf7a434a0b393da3c0fb186365be3e18f952f2fd40bd51fb" }, { "path": "tests/bootstrap.php", "sha256": "cbfefee52510e920fdbd8d58e8943e03a01c507f8a41e365273eb1e2b20a60ba" } ] @@ -327,8 +327,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "3d520cbaf53c97c701ee1cb2f729bc09466f2ebb5ee262e2dde5f6e0fbfdaa39" }, - { "path": "composer.lock", "sha256": "0602d3f55992efbb407e66081d9483ac370c42c7cdbe0a9a1011d8ba0df080a6" }, + { "path": "composer.json", "sha256": "1105e49fadd16917daa3a01543dc85488f2ceeed6174d92c6b31e749695f832c" }, + { "path": "composer.lock", "sha256": "5413458c84f6815cafee6ea255051300bdcc0905f64389699697f55959a3b065" }, { "path": "phpunit.xml.dist", "sha256": "1f1877783a07ed91172dfbb0c7ce9b42c7246f5c4b5dcd7ee1d479528fe09861" }, { "path": "tests/bootstrap.php", "sha256": "503a09317a0def999c45f81478aca9cf0b165512efcaac1b47d52512b120b8ac" } ] @@ -362,8 +362,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "c76d358c2ad88cfaa343d3e3910207d536243de020b2d99a887a609b9d7d7859" }, - { "path": "composer.lock", "sha256": "5b604320f1628a9ac63b8bdfcc79a900da71f39b0aca990fa3952e2fab99d955" }, + { "path": "composer.json", "sha256": "ac1788c75e0b6e45618cb8c26eb1158a9c9fcf48590eac6df1d2c9346dffeccb" }, + { "path": "composer.lock", "sha256": "deb1e089550dde49dee2d3cafc124e9634f8f591e9acbc482a09baa60007a35a" }, { "path": "phpunit.xml.dist", "sha256": "c23c3afa83ae468cd04eb74991e71e9a69f95bf8520a54d9c5689b28e302a365" }, { "path": "tests/bootstrap.php", "sha256": "45ef4fbfb2c85f4bab2c7bcd38218c59b01742d1fe86e61e30f1058f66134f8a" } ] @@ -397,8 +397,8 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "184e63eddd9ea61eb9f39e58e918798767535cb5c8ac71c4ff48ace433bdbe19" }, - { "path": "composer.lock", "sha256": "cd508765eb8870da29ba6ac8021d30feca2264895adff8242a26c9f812ed4c4c" }, + { "path": "composer.json", "sha256": "58756a64309c8450a8d2b280b8533f87cb21406cb8304c86c3655ab70a6b814a" }, + { "path": "composer.lock", "sha256": "4a70277a3cf46ea5c2621b5d943a990475448bbd3fcc86314320f7c42d7664b2" }, { "path": "phpunit.xml.dist", "sha256": "3e0a40a7d519b655d28bfe84c1e1018d9363f61a321bf005a07c4968de944066" }, { "path": "tests/bootstrap.php", "sha256": "3e9ce13d43d8fb1742634de5dad4a6388700c23419d461279232113c4cae4d77" } ] @@ -426,10 +426,10 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "e431d9aed0f0b39bc4e1729e0e6da64dc4da5e859ea032c7feb44489837310fc" }, - { "path": "composer.lock", "sha256": "f83d953a43c60c7b6e22290078ccad0b33466e11a7a5d5fd7c13f3dadd5fe4dd" }, + { "path": "composer.json", "sha256": "28c221775e62d1ac31999681d0ec51e3fd7398a44e84aabc4d3be9e6982ea7ab" }, + { "path": "composer.lock", "sha256": "6b93339b33dff6de43f7507f2f50f2fcdae4eee14547c428b65f024f108d2d9a" }, { "path": "phpunit.xml.dist", "sha256": "78d45dc8e6316665d752afa2ed801c82742201f512d06bc76464556d5445baa6" }, - { "path": "tests/bootstrap.php", "sha256": "bf157f2399a9b9e5757239e921bcf4cb22ecd31b65915409d0209b1d4bc281e1" } + { "path": "tests/bootstrap.php", "sha256": "babaefde10d66233bb531111409fd87c72beeb4ed70575bf62626e0227bf13f0" } ] } }, @@ -461,10 +461,10 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "0a0ba88076a3c9e311dbfd66b9da226a3e3f7c2f42fcda416e64192c4fd00b9a" }, - { "path": "composer.lock", "sha256": "dfa7412474360ee12e80ec6ddc1965314b461f1944634ec3eafea0c868f36a04" }, + { "path": "composer.json", "sha256": "4aa3c793c7ed616b84abc8a0eaa39aedc3e7f0260fb8b3aa1937a6b01d1141ed" }, + { "path": "composer.lock", "sha256": "c9b97f0f32b758a82c532f46127c9f245927ce4142358b214fefac584d045e69" }, { "path": "phpunit.xml.dist", "sha256": "51c99ea65a60b1af445c05fe9a78a13eaba1ad9be6b1032635ef691ea9fa0d4e" }, - { "path": "tests/bootstrap.php", "sha256": "1255c3bb96672e05c7cf1c9e541107e9ea5eb828cb61693d7544a994532f46c6" } + { "path": "tests/bootstrap.php", "sha256": "ac2db4be18f7c6fdec1cbd81aba1927f2426a6b70110972750089ff5dc93d2b3" } ] } }, @@ -496,10 +496,10 @@ "phpunit": { "config": "phpunit.xml.dist", "files": [ - { "path": "composer.json", "sha256": "953f8a120136cd4e1ff639b7cba0cc51746a7c52df9a8706b6871a272ea11275" }, - { "path": "composer.lock", "sha256": "63fe118ffb61c32823bf3993f406c42c2da90a5607ef7ead4e1db268359cb651" }, + { "path": "composer.json", "sha256": "385b2ef780f65389932fe0750a18a4d7d902158c5e23f5aaa582d0aa61e77d5f" }, + { "path": "composer.lock", "sha256": "7258d903cacc8a4646266a86718ed6921c2fd06ca1276c3119c2153886a2954a" }, { "path": "phpunit.xml.dist", "sha256": "ce4fc4cab25c42bd9e4d8f432dbbbf577a093192d60cf388bb2ddcdcff7c34a7" }, - { "path": "tests/bootstrap.php", "sha256": "8ca8b217e5c06cad564c44d1fe1ad747a1f0878de6055ab492a32dadd9d8f194" } + { "path": "tests/bootstrap.php", "sha256": "d56e578a35669ef19e49e5a1c6e925b69c8d389cc3427aaef67e3d5ab51c9946" } ] } }, diff --git a/scripts/check-static-analysis-baselines.mjs b/scripts/check-static-analysis-baselines.mjs index 5cf6af6..845760f 100644 --- a/scripts/check-static-analysis-baselines.mjs +++ b/scripts/check-static-analysis-baselines.mjs @@ -671,6 +671,37 @@ function isMonotonicWordPressMinimumChange(base, head) { && compareDottedVersions(headConfig.value, baseConfig.value) > 0; } +function phpstanLevel(source) { + if (source === null || source.includes('\r')) return null; + const matches = [...source.matchAll(/^[ \t]+level: ([0-9]+)$/gmu)]; + if (matches.length !== 1) return null; + const level = Number(matches[0][1]); + return Number.isSafeInteger(level) && level <= 10 ? level : null; +} + +function isClearedPhpstanBaselineMigration(base, head) { + const baseLevel = phpstanLevel(base); + const headLevel = phpstanLevel(head); + if (baseLevel === null || headLevel === null || headLevel <= baseLevel) return false; + + const baselineInclude = /^includes:\n[ \t]+- phpstan-baseline\.neon\n\n/u; + if (!baselineInclude.test(base) || head.includes('phpstan-baseline.neon') || head.includes('ignoreErrors')) { + return false; + } + + // Permit only the WordPress compatibility stub used by the level-7 migration. + // The following quality step still runs the unchanged Composer analyzer command. + const stubInclude = ' stubFiles:\n - phpstan-wordpress-compat.stub\n'; + let normalizedHead = head; + if (!base.includes('stubFiles:') && head.includes(stubInclude)) { + if (readHead('phpstan-wordpress-compat.stub') === null) return false; + normalizedHead = head.replace(stubInclude, ''); + } + + const normalizeLevel = (source) => source.replace(/^([ \t]+level: )[0-9]+$/mu, '$1__LEVEL__'); + return normalizeLevel(base.replace(baselineInclude, '')) === normalizeLevel(normalizedHead); +} + function protectIntroducedAnalyzerContract(baselinePath, centralPhpcs = false) { if (baselinePath === 'phpcs-baseline.json' && centralPhpcs) return; const contract = ANALYZER_CONTRACTS[baselinePath]; @@ -712,7 +743,7 @@ function protectIntroducedAnalyzerContract(baselinePath, centralPhpcs = false) { } } -function protectAnalyzerContract(baseRevision, baselinePath, centralPhpcs = false) { +function protectAnalyzerContract(baseRevision, baselinePath, centralPhpcs = false, baselineRemoved = false) { if (baselinePath === 'phpcs-baseline.json' && centralPhpcs) return; const contract = ANALYZER_CONTRACTS[baselinePath]; const baseComposer = parseComposer(readAtRevision(baseRevision, 'composer.json'), baselinePath); @@ -727,6 +758,15 @@ function protectAnalyzerContract(baseRevision, baselinePath, centralPhpcs = fals let phpcsMinimumChangeUsed = false; for (const path of contract.configurations) { + if (baselinePath === 'phpstan-baseline.neon' + && baselineRemoved + && baseConfigurations.length === 1 + && headConfigurations.length === 1 + && baseConfigurations[0] === path + && headConfigurations[0] === path + && isClearedPhpstanBaselineMigration(readAtRevision(baseRevision, path), readHead(path))) { + continue; + } if (baselinePath === 'phpcs-baseline.json' && isMonotonicWordPressMinimumChange(readAtRevision(baseRevision, path), readHead(path))) { if (baseConfigurations.length !== 1 @@ -780,7 +820,7 @@ export function checkBaselines(baseRevision, { centralPhpcs = false } = {}) { continue; } - protectAnalyzerContract(baseRevision, path, centralPhpcs); + protectAnalyzerContract(baseRevision, path, centralPhpcs, headSource === null); const base = parse(baseSource, `${path} at ${baseRevision}`); const head = headSource === null ? new Map() : parse(headSource, path); diff --git a/tests/integration-check-policy.test.mjs b/tests/integration-check-policy.test.mjs index ecabba7..4915f80 100644 --- a/tests/integration-check-policy.test.mjs +++ b/tests/integration-check-policy.test.mjs @@ -157,8 +157,8 @@ test('only reviewed WordPress integration pilots are enrolled', () => { assert.deepEqual(userAvatars.managed_paths, ['ci', 'release', 'dependabot']); assert.equal(userAvatars.checks.phpunit.config, 'phpunit.xml.dist'); assert.deepEqual(userAvatars.checks.phpunit.files, [ - { path: 'composer.json', sha256: '3d520cbaf53c97c701ee1cb2f729bc09466f2ebb5ee262e2dde5f6e0fbfdaa39' }, - { path: 'composer.lock', sha256: '0602d3f55992efbb407e66081d9483ac370c42c7cdbe0a9a1011d8ba0df080a6' }, + { path: 'composer.json', sha256: '1105e49fadd16917daa3a01543dc85488f2ceeed6174d92c6b31e749695f832c' }, + { path: 'composer.lock', sha256: '5413458c84f6815cafee6ea255051300bdcc0905f64389699697f55959a3b065' }, { path: 'phpunit.xml.dist', sha256: '1f1877783a07ed91172dfbb0c7ce9b42c7246f5c4b5dcd7ee1d479528fe09861' }, { path: 'tests/bootstrap.php', sha256: '503a09317a0def999c45f81478aca9cf0b165512efcaac1b47d52512b120b8ac' }, ]); @@ -181,8 +181,8 @@ test('only reviewed WordPress integration pilots are enrolled', () => { assert.deepEqual(userPreferences.managed_paths, ['ci', 'release', 'dependabot']); assert.equal(userPreferences.checks.phpunit.config, 'phpunit.xml.dist'); assert.deepEqual(userPreferences.checks.phpunit.files, [ - { path: 'composer.json', sha256: 'c76d358c2ad88cfaa343d3e3910207d536243de020b2d99a887a609b9d7d7859' }, - { path: 'composer.lock', sha256: '5b604320f1628a9ac63b8bdfcc79a900da71f39b0aca990fa3952e2fab99d955' }, + { path: 'composer.json', sha256: 'ac1788c75e0b6e45618cb8c26eb1158a9c9fcf48590eac6df1d2c9346dffeccb' }, + { path: 'composer.lock', sha256: 'deb1e089550dde49dee2d3cafc124e9634f8f591e9acbc482a09baa60007a35a' }, { path: 'phpunit.xml.dist', sha256: 'c23c3afa83ae468cd04eb74991e71e9a69f95bf8520a54d9c5689b28e302a365' }, { path: 'tests/bootstrap.php', sha256: '45ef4fbfb2c85f4bab2c7bcd38218c59b01742d1fe86e61e30f1058f66134f8a' }, ]); diff --git a/tests/static-analysis-baselines-integration.test.mjs b/tests/static-analysis-baselines-integration.test.mjs index 0d99137..fbd1c98 100644 --- a/tests/static-analysis-baselines-integration.test.mjs +++ b/tests/static-analysis-baselines-integration.test.mjs @@ -117,6 +117,65 @@ test('existing baseline protects conventional analyzer configurations', (t) => { assert.match(result.stderr, /may not add, remove, or change phpstan\.neon/u); }); +function phpstanRetirementRepository() { + return createRepository({ + 'composer.json': JSON.stringify({ scripts: { phpstan: 'phpstan analyse --no-progress' } }, null, 2) + '\n', + 'phpstan-baseline.neon': canonicalPhpstanBaseline, + 'phpstan.neon.dist': [ + 'includes:', + ' - phpstan-baseline.neon', + '', + 'parameters:', + ' level: 5', + ' phpVersion: 70400', + ' paths:', + ' - includes', + '', + ].join('\n'), + }); +} + +function retirePhpstanBaseline(fixture, { level = 7, path = 'includes', stub = true, ignoreErrors = false } = {}) { + unlinkSync(join(fixture.root, 'phpstan-baseline.neon')); + writeFixtureFile(fixture.root, 'phpstan.neon.dist', [ + 'parameters:', + ' level: ' + level, + ' phpVersion: 70400', + ' paths:', + ' - ' + path, + ' stubFiles:', + ' - phpstan-wordpress-compat.stub', + ...(ignoreErrors ? [' ignoreErrors: []'] : []), + '', + ].join('\n')); + if (stub) writeFixtureFile(fixture.root, 'phpstan-wordpress-compat.stub', ' { + const fixture = phpstanRetirementRepository(); + t.after(() => rmSync(fixture.root, { force: true, recursive: true })); + retirePhpstanBaseline(fixture); + const result = runChecker(fixture.root, fixture.revision); + assert.equal(result.status, 0, result.stderr); +}); + +test('cleared PHPStan baseline still rejects analyzer configuration drift', (t) => { + for (const variant of [ + { name: 'unchanged level', level: 5 }, + { name: 'lower level', level: 4 }, + { name: 'changed paths', path: 'tests' }, + { name: 'missing stub', stub: false }, + { name: 'new ignored errors', ignoreErrors: true }, + ]) { + const fixture = phpstanRetirementRepository(); + t.after(() => rmSync(fixture.root, { force: true, recursive: true })); + retirePhpstanBaseline(fixture, variant); + const result = runChecker(fixture.root, fixture.revision); + assert.equal(result.status, 2, variant.name + ': ' + result.stderr); + assert.match(result.stderr, /may not add, remove, or change phpstan\.neon/u); + } +}); + test('existing baseline protects a directly named repository runner', (t) => { const fixture = createRepository({ 'composer.json': `${JSON.stringify({ scripts: { phpcs: 'php scripts/check-phpcs-baseline.php' } }, null, 2)}\n`,