From ff6f0789e51f56b1ea97ce60156b77314e493f57 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 16 Sep 2026 22:58:58 +0900 Subject: [PATCH 1/5] fix(ci): write the Sonar exclusions as path patterns, not directory prefixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sonar.exclusions=skills/spring-docs/scripts/` (ee09832) matched nothing. SonarCloud matches these values against whole file paths, so a bare directory prefix is discarded in silence: `main` still indexed 5,018 lines of generated bundle and carried 359 of its 412 open findings inside the directory the exclusion was supposed to cover — 332 of them `fast-xml-parser`'s entity tables, inlined into `detect.mjs` and uneditable because `build:skill:check` byte-compares the bundle against a fresh build. The file's own header caused it: it read the Automatic Analysis page's "Wildcard patterns are not allowed" as covering every value here. That line is about the plain path lists `sonar.sources` and `sonar.tests`; the exclusion properties take patterns, and a whole subtree is written `dir/**/*`. Header corrected alongside the values. `.codacy.yml` needed no change — `skills/*/scripts/**` is already Java glob and Codacy reports 0 of its 211 findings under `skills/`. Add a test asserting each configured pattern matches the committed bundles. A pattern that matches nothing is indistinguishable from one that works by reading it, which is how this survived review once already. Refs #23 --- .sonarcloud.properties | 22 ++++-- .../generated-bundle-exclusions.test.ts | 68 +++++++++++++++++++ 2 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 scripts/__tests__/generated-bundle-exclusions.test.ts diff --git a/.sonarcloud.properties b/.sonarcloud.properties index dca9717..1bf1495 100644 --- a/.sonarcloud.properties +++ b/.sonarcloud.properties @@ -1,15 +1,22 @@ # SonarQube Cloud runs here in Automatic Analysis mode — no workflow invokes a # scanner, the GitHub App analyses every push on its own. That mode reads this -# file and ignores `sonar-project.properties`, and it accepts no wildcards in -# these values, so paths are written as plain directory prefixes. +# file and ignores `sonar-project.properties`. # -# Two consequences worth knowing before editing: +# Three consequences worth knowing before editing: # - Only the copy on the default branch takes effect. A change here does not # alter the analysis of the pull request that makes it; it applies once # merged. # - Where this file and the SonarQube Cloud UI disagree, this file wins. An # entry that matches nothing therefore cannot be corrected from the UI while # it is still here. +# - The exclusion values below are path *patterns* matched against whole file +# paths, not directory prefixes: `*` matches within one path segment, `**` +# spans directories, and a whole subtree is written `dir/**/*`. A bare +# `dir/` matches no file and is discarded in silence. The Automatic Analysis +# page's "Wildcard patterns are not allowed" is about the plain path lists +# `sonar.sources` and `sonar.tests`; ee09832 read it as covering these too +# and wrote bare prefixes, which left the exclusion inert — `main` still +# indexed 5,018 lines of generated code and reported 359 findings in it. # Generated by `bun run build:skill` from `scripts/*.ts`, committed because the # `npx skills` install channel copies only the skill directory and runs no @@ -17,7 +24,8 @@ # of `scripts/docs.ts` reappears in the bundle, which read as 7.7% duplication # on new code, and the `var` declarations Bun emits cost a reliability rating # that no edit can recover — `bun run build:skill:check` byte-compares the -# bundle against a fresh build, so hand-editing it fails CI. -# `eslint.config.js` ignores the same directory, for the same reason. -sonar.exclusions=skills/spring-docs/scripts/ -sonar.cpd.exclusions=skills/spring-docs/scripts/ +# bundle against a fresh build, so hand-editing it fails CI. `detect.mjs` also +# inlines `fast-xml-parser`, whose entity tables read as first-party source. +# `eslint.config.js` and `.codacy.yml` exclude the same directories. +sonar.exclusions=skills/*/scripts/**/* +sonar.cpd.exclusions=skills/*/scripts/**/* diff --git a/scripts/__tests__/generated-bundle-exclusions.test.ts b/scripts/__tests__/generated-bundle-exclusions.test.ts new file mode 100644 index 0000000..8028286 --- /dev/null +++ b/scripts/__tests__/generated-bundle-exclusions.test.ts @@ -0,0 +1,68 @@ +/** + * Guards the analyser exclusions that keep `skills//scripts/` out of Sonar, + * Codacy and ESLint. + * + * The exclusion in ee09832 was written as a bare directory prefix + * (`skills/spring-docs/scripts/`). Every analyser matches patterns against the + * whole file path, so that prefix matched nothing and was dropped without a + * warning — SonarCloud kept indexing 5,018 generated lines for two days before + * anyone noticed. A pattern that matches no file looks identical to a pattern + * that works, so assert the match instead of reading the config. + * + * `Bun.Glob` stands in for three matchers it is not, so a pass is not proof + * that SonarCloud reads a pattern the same way. What it does catch is the + * failure that actually happened: a pattern that matches no bundle at all. + */ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' + +import { describe, expect, test } from 'bun:test' + +const ROOT = join(import.meta.dir, '..', '..') + +/** The committed bundles every exclusion below is supposed to cover. */ +const BUNDLES = Array.from(new Bun.Glob('skills/*/scripts/*.mjs').scanSync(ROOT), p => p.replaceAll('\\', '/')) + +function read(file: string): string { + return readFileSync(join(ROOT, file), 'utf8') +} + +/** Values of a `key=a,b` line in a `.properties` file. */ +function properties(source: string, key: string): string[] { + const line = source.split('\n').find(l => l.startsWith(`${key}=`)) + return line ? line.slice(key.length + 1).split(',').filter(Boolean) : [] +} + +/** Every single-quoted `skills/.../scripts/...` pattern in a config file. */ +function skillPatterns(source: string): string[] { + return Array.from(source.matchAll(/'(skills\/[^']*scripts[^']*)'/g), m => m[1]!) +} + +function expectCoversBundles(patterns: string[]): void { + expect(patterns.length).toBeGreaterThan(0) + for (const pattern of patterns) { + const glob = new Bun.Glob(pattern) + for (const bundle of BUNDLES) expect([pattern, bundle, glob.match(bundle)]).toEqual([pattern, bundle, true]) + } +} + +describe('generated bundle exclusions', () => { + test('the bundles the exclusions target are committed', () => { + // A Set, because `Bun.Glob.scanSync` fixes no traversal order. + expect(new Set(BUNDLES)).toEqual(new Set(['skills/spring-docs/scripts/detect.mjs', 'skills/spring-docs/scripts/docs.mjs'])) + }) + + test('.sonarcloud.properties excludes them from issues and duplication', () => { + const source = read('.sonarcloud.properties') + expectCoversBundles(properties(source, 'sonar.exclusions')) + expectCoversBundles(properties(source, 'sonar.cpd.exclusions')) + }) + + test('.codacy.yml excludes them', () => { + expectCoversBundles(skillPatterns(read('.codacy.yml'))) + }) + + test('eslint.config.js ignores them', () => { + expectCoversBundles(skillPatterns(read('eslint.config.js'))) + }) +}) From fddb91e3c40800340770b5332c158d889f45c75f Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 16 Sep 2026 23:31:54 +0900 Subject: [PATCH 2/5] chore: apply AI code review suggestions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three review findings on the exclusion guard, all on the same weakness — the test read configuration as text, and text cannot tell an active setting from a dead one. - Read the *active* exclusion, not the file's raw text (gpt, important). A commented-out `exclude_paths` entry or eslint `ignores` line still contains a pattern that matches the bundles, so the guard passed while the analyser received nothing — defeated by exactly the edit it exists to catch. ESLint's ignores now come off the evaluated config; the other two off a line a leading `#` disqualifies. Verified by mutation: commenting out either entry, or restoring the old bare prefix, fails the guard. - Parse the properties file CRLF-safely (gemini-code-assist). No `.gitattributes`, so a Windows checkout with `core.autocrlf=true` left a `\r` on every pattern and failed the test spuriously. - Correct the stale guidance in `.please/docs/knowledge/gotchas.md` (greptile). It still carried the "wildcards are not allowed" misreading that produced the broken exclusion, which is how a future maintainer reintroduces it. Reading the configs through Bun's file and glob APIs also clears the two Codacy findings this PR added (`no-non-null-assertion`, `detect-non-literal-fs-filename`). Not applied: `scanSync({ cwd: ROOT })` (gemini-code-assist). Bun types the string form itself — `scanSync(optionsOrCwd?: string | GlobScanOptions)`. --- .please/docs/knowledge/gotchas.md | 4 +- .../generated-bundle-exclusions.test.ts | 71 ++++++++++++++----- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/.please/docs/knowledge/gotchas.md b/.please/docs/knowledge/gotchas.md index 9641573..3492f44 100644 --- a/.please/docs/knowledge/gotchas.md +++ b/.please/docs/knowledge/gotchas.md @@ -22,7 +22,9 @@ Neither service is invoked by a workflow — both analyse every push through their GitHub App. That changes where their configuration lives and when it takes effect. -- **SonarQube Cloud is in Automatic Analysis mode, which reads `.sonarcloud.properties` and ignores `sonar-project.properties`.** The docs are explicit that the two files are different and that a `sonar-project.properties` in an imported project is ignored. Three further constraints: only the copy on the **default branch** applies (a change does not affect the PR that makes it), **wildcards are not allowed** in the values, and where the file and the SonarQube Cloud UI disagree the **file wins** — so an entry that matches nothing cannot be corrected from the UI while it is still there. +- **SonarQube Cloud is in Automatic Analysis mode, which reads `.sonarcloud.properties` and ignores `sonar-project.properties`.** The docs are explicit that the two files are different and that a `sonar-project.properties` in an imported project is ignored. Three further constraints: only the copy on the **default branch** applies (a change does not affect the PR that makes it), the **exclusion values are path patterns, not directory prefixes**, and where the file and the SonarQube Cloud UI disagree the **file wins** — so an entry that matches nothing cannot be corrected from the UI while it is still there. + +- **The [Automatic Analysis page](https://docs.sonarsource.com/sonarqube-cloud/analyzing-source-code/automatic-analysis/)'s "Wildcard patterns are not allowed" is about `sonar.sources` and `sonar.tests`, not the exclusions.** Those two take plain path lists; `sonar.exclusions` and `sonar.cpd.exclusions` take [path patterns](https://docs.sonarsource.com/sonarqube-cloud/managing-your-projects/project-analysis/setting-analysis-scope/excluding-files-based-on-patterns/) matched against whole file paths, where a whole subtree is written `dir/**/*` and a bare `dir/` matches nothing and is discarded in silence. Reading that line as covering every value in the file is what made ee09832's exclusion inert: `main` kept indexing 5,018 lines of generated bundle and reported 359 of its 412 findings inside the directory that was supposed to be excluded, until #26. `scripts/__tests__/generated-bundle-exclusions.test.ts` now fails on any exclusion pattern that matches no committed bundle — a pattern matching nothing is indistinguishable from a working one by reading it. - **Codacy reads `.codacy.yml` (or `.codacy.yaml`), and the first line must be `---`.** Additions are honoured on the PR that makes them; only removals wait for the default branch. Once the file exists, the UI's "Ignored files" settings stop applying. Validate before pushing: `docker run --rm -v "$(pwd)":/src codacy/codacy-analysis-cli validate-configuration --directory /src`. diff --git a/scripts/__tests__/generated-bundle-exclusions.test.ts b/scripts/__tests__/generated-bundle-exclusions.test.ts index 8028286..9bb2a33 100644 --- a/scripts/__tests__/generated-bundle-exclusions.test.ts +++ b/scripts/__tests__/generated-bundle-exclusions.test.ts @@ -9,12 +9,18 @@ * anyone noticed. A pattern that matches no file looks identical to a pattern * that works, so assert the match instead of reading the config. * + * Each reader below takes the *active* setting, never the file's raw text: a + * commented-out exclusion still contains a pattern that would match, so a text + * scan passes while the analyser receives nothing — the same blind spot one + * level down. ESLint's is read from the evaluated config, and the other two + * from a line that a leading `#` disqualifies. + * * `Bun.Glob` stands in for three matchers it is not, so a pass is not proof * that SonarCloud reads a pattern the same way. What it does catch is the * failure that actually happened: a pattern that matches no bundle at all. */ -import { readFileSync } from 'node:fs' import { join } from 'node:path' +import { pathToFileURL } from 'node:url' import { describe, expect, test } from 'bun:test' @@ -23,19 +29,49 @@ const ROOT = join(import.meta.dir, '..', '..') /** The committed bundles every exclusion below is supposed to cover. */ const BUNDLES = Array.from(new Bun.Glob('skills/*/scripts/*.mjs').scanSync(ROOT), p => p.replaceAll('\\', '/')) -function read(file: string): string { - return readFileSync(join(ROOT, file), 'utf8') +/** + * Split on `\r?\n`, not `\n`: the repository carries no `.gitattributes`, so a + * Windows checkout with `core.autocrlf=true` leaves a `\r` on every value, and + * a pattern ending in `\r` matches no bundle — a false failure that reads + * exactly like the misconfiguration this file exists to catch. + */ +async function lines(file: string): Promise { + return (await Bun.file(join(ROOT, file)).text()).split(/\r?\n/) +} + +/** The patterns `.sonarcloud.properties` sets for `key`, as a `key=a,b` line. */ +async function sonarExclusions(key: string): Promise { + // A commented-out setting starts with `#`, so it never matches the key prefix. + const line = (await lines('.sonarcloud.properties')).find(l => l.startsWith(`${key}=`)) + return line ? line.slice(key.length + 1).split(',').map(v => v.trim()).filter(Boolean) : [] } -/** Values of a `key=a,b` line in a `.properties` file. */ -function properties(source: string, key: string): string[] { - const line = source.split('\n').find(l => l.startsWith(`${key}=`)) - return line ? line.slice(key.length + 1).split(',').filter(Boolean) : [] +/** The `skills/` entries of `.codacy.yml`'s `exclude_paths:` block. */ +async function codacyExclusions(): Promise { + const source = await lines('.codacy.yml') + const start = source.findIndex(l => l.startsWith('exclude_paths:')) + expect(start).toBeGreaterThanOrEqual(0) + + const patterns: string[] = [] + for (const line of source.slice(start + 1)) { + const entry = /^\s+-\s*'([^']+)'/.exec(line) + if (entry) { + patterns.push(entry[1] ?? '') + continue + } + // A comment or a blank line sits inside the block; anything else ends it. + if (line.trim() !== '' && !line.trimStart().startsWith('#')) + break + } + return patterns.filter(p => p.startsWith('skills/')) } -/** Every single-quoted `skills/.../scripts/...` pattern in a config file. */ -function skillPatterns(source: string): string[] { - return Array.from(source.matchAll(/'(skills\/[^']*scripts[^']*)'/g), m => m[1]!) +/** The `skills/` ignore patterns ESLint actually loads, off the evaluated config. */ +async function eslintIgnores(): Promise { + const config = (await import(pathToFileURL(join(ROOT, 'eslint.config.js')).href)) as { + default: { ignores?: string[] }[] + } + return config.default.flatMap(entry => entry.ignores ?? []).filter(p => p.startsWith('skills/')) } function expectCoversBundles(patterns: string[]): void { @@ -52,17 +88,16 @@ describe('generated bundle exclusions', () => { expect(new Set(BUNDLES)).toEqual(new Set(['skills/spring-docs/scripts/detect.mjs', 'skills/spring-docs/scripts/docs.mjs'])) }) - test('.sonarcloud.properties excludes them from issues and duplication', () => { - const source = read('.sonarcloud.properties') - expectCoversBundles(properties(source, 'sonar.exclusions')) - expectCoversBundles(properties(source, 'sonar.cpd.exclusions')) + test('.sonarcloud.properties excludes them from issues and duplication', async () => { + expectCoversBundles(await sonarExclusions('sonar.exclusions')) + expectCoversBundles(await sonarExclusions('sonar.cpd.exclusions')) }) - test('.codacy.yml excludes them', () => { - expectCoversBundles(skillPatterns(read('.codacy.yml'))) + test('.codacy.yml excludes them', async () => { + expectCoversBundles(await codacyExclusions()) }) - test('eslint.config.js ignores them', () => { - expectCoversBundles(skillPatterns(read('eslint.config.js'))) + test('eslint.config.js ignores them', async () => { + expectCoversBundles(await eslintIgnores()) }) }) From fa7036cd4173c5418f1fe919f2a373fd51dcd333 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 16 Sep 2026 23:36:47 +0900 Subject: [PATCH 3/5] chore: clear the Codacy findings the guard test introduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codacy's ESLint runs its own rule set, so the previous commit traded two of its findings for two others: `no-unnecessary-condition` on `entry[1] ?? ''` (its type view lacks this repo's `noUncheckedIndexedAccess`, so the `??` reads as dead), and `no-unsanitized/method` on a dynamic `import()` with a computed specifier. Both go away without weakening the check. The Codacy block parser matches the quoted entry with `startsWith`/`endsWith` instead of a capture group, and the ESLint ignores are read as text with `//` lines dropped rather than by importing the module — a literal-specifier import would satisfy the rule but needs `allowJs` in `tsconfig.json`, which is a project-wide change to suit one test. Dropping comment lines is what closes the gap gpt found; evaluating the module would only have added coverage for a pattern built at runtime, which this config does not do. Mutation-checked: the bare `skills/spring-docs/scripts/` prefix, a commented-out `.codacy.yml` entry, and a commented-out eslint `ignores` line each fail the guard. --- .../generated-bundle-exclusions.test.ts | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/scripts/__tests__/generated-bundle-exclusions.test.ts b/scripts/__tests__/generated-bundle-exclusions.test.ts index 9bb2a33..3c15c57 100644 --- a/scripts/__tests__/generated-bundle-exclusions.test.ts +++ b/scripts/__tests__/generated-bundle-exclusions.test.ts @@ -12,15 +12,14 @@ * Each reader below takes the *active* setting, never the file's raw text: a * commented-out exclusion still contains a pattern that would match, so a text * scan passes while the analyser receives nothing — the same blind spot one - * level down. ESLint's is read from the evaluated config, and the other two - * from a line that a leading `#` disqualifies. + * level down. Each reader therefore drops the file's comment lines before it + * takes anything from them. * * `Bun.Glob` stands in for three matchers it is not, so a pass is not proof * that SonarCloud reads a pattern the same way. What it does catch is the * failure that actually happened: a pattern that matches no bundle at all. */ import { join } from 'node:path' -import { pathToFileURL } from 'node:url' import { describe, expect, test } from 'bun:test' @@ -54,24 +53,32 @@ async function codacyExclusions(): Promise { const patterns: string[] = [] for (const line of source.slice(start + 1)) { - const entry = /^\s+-\s*'([^']+)'/.exec(line) - if (entry) { - patterns.push(entry[1] ?? '') + const entry = line.trim() + if (entry.startsWith('- \'') && entry.endsWith('\'')) { + patterns.push(entry.slice(3, -1)) continue } // A comment or a blank line sits inside the block; anything else ends it. - if (line.trim() !== '' && !line.trimStart().startsWith('#')) + if (entry !== '' && !entry.startsWith('#')) break } return patterns.filter(p => p.startsWith('skills/')) } -/** The `skills/` ignore patterns ESLint actually loads, off the evaluated config. */ +/** + * The `skills/` ignore patterns in `eslint.config.js`'s `ignores` array. + * + * Read as text with comment lines dropped, not by importing the module. The + * evaluated config would be the stronger source, but reaching it needs either a + * non-literal dynamic `import()` or `allowJs` in `tsconfig.json`, and neither is + * worth a project-wide change here. Dropping `//` lines closes the gap that + * matters: a commented-out ignore no longer supplies a pattern that passes. + */ async function eslintIgnores(): Promise { - const config = (await import(pathToFileURL(join(ROOT, 'eslint.config.js')).href)) as { - default: { ignores?: string[] }[] - } - return config.default.flatMap(entry => entry.ignores ?? []).filter(p => p.startsWith('skills/')) + return (await lines('eslint.config.js')) + .map(l => l.trim()) + .filter(l => !l.startsWith('//') && l.startsWith('\'skills/')) + .map(l => l.slice(1, l.indexOf('\'', 1))) } function expectCoversBundles(patterns: string[]): void { From 8ab4a6a96af50e2b7183ee662bbd07c996c62ee3 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 16 Sep 2026 23:38:54 +0900 Subject: [PATCH 4/5] chore: tolerate harmless reformatting in the config readers gemini-code-assist flagged both readers as brittle, and it is right: neither misreads a config, but each fails the whole guard on a formatting change that leaves the configuration valid. `.properties` permits whitespace around the `=` and before the key, and YAML permits double-quoted or unquoted scalars, so the sonar reader now anchors on a whitespace-tolerant pattern and the Codacy reader strips whichever quoting style it finds. Implemented without the suggested capture-group regex, which would have reintroduced the `entry[1] ?? ''` that Codacy's `no-unnecessary-condition` rejected one commit ago. Mutation-checked, five scenarios: spaces around `=` and double-quoted YAML now pass; the bare `skills/spring-docs/scripts/` prefix, a commented-out `.codacy.yml` entry, and a commented-out eslint `ignores` line still fail. --- .../generated-bundle-exclusions.test.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/scripts/__tests__/generated-bundle-exclusions.test.ts b/scripts/__tests__/generated-bundle-exclusions.test.ts index 3c15c57..362d514 100644 --- a/scripts/__tests__/generated-bundle-exclusions.test.ts +++ b/scripts/__tests__/generated-bundle-exclusions.test.ts @@ -38,11 +38,18 @@ async function lines(file: string): Promise { return (await Bun.file(join(ROOT, file)).text()).split(/\r?\n/) } -/** The patterns `.sonarcloud.properties` sets for `key`, as a `key=a,b` line. */ +/** + * The patterns `.sonarcloud.properties` sets for `key`, as a `key=a,b` line. + * + * `.properties` allows whitespace around the `=` and before the key, so match + * that rather than a bare prefix — a reformatted file should not fail the + * guard. A commented-out setting starts with `#`, which the anchor rejects. + */ async function sonarExclusions(key: string): Promise { - // A commented-out setting starts with `#`, so it never matches the key prefix. - const line = (await lines('.sonarcloud.properties')).find(l => l.startsWith(`${key}=`)) - return line ? line.slice(key.length + 1).split(',').map(v => v.trim()).filter(Boolean) : [] + const setting = new RegExp(`^\\s*${key.replaceAll('.', '\\.')}\\s*=`) + const line = (await lines('.sonarcloud.properties')).find(l => setting.test(l)) + // The first `=` is the separator; no exclusion pattern contains one. + return line ? line.slice(line.indexOf('=') + 1).split(',').map(v => v.trim()).filter(Boolean) : [] } /** The `skills/` entries of `.codacy.yml`'s `exclude_paths:` block. */ @@ -54,8 +61,9 @@ async function codacyExclusions(): Promise { const patterns: string[] = [] for (const line of source.slice(start + 1)) { const entry = line.trim() - if (entry.startsWith('- \'') && entry.endsWith('\'')) { - patterns.push(entry.slice(3, -1)) + if (entry.startsWith('- ')) { + // Strip the quotes YAML does not require here; single, double, or none. + patterns.push(entry.slice(2).trim().replace(/^["']|["']$/g, '')) continue } // A comment or a blank line sits inside the block; anything else ends it. From 76599f89654e137fea7ce5661b2ec31462d9bed9 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Wed, 16 Sep 2026 23:45:08 +0900 Subject: [PATCH 5/5] chore: drop the dynamic RegExp from the properties reader Building the key matcher with `new RegExp` tripped three Codacy rules at once (`security/detect-non-literal-regexp`, `security-node/non-literal-reg-expr`, `javascript_dos/rule-non-literal-regexp`), all on the constructor taking a template literal. Splitting each line on its first `=` and comparing the trimmed key needs no regex at all, and it is the simpler statement of the same intent: it still tolerates whitespace around the separator and before the key, and a commented-out setting still keeps its `#` inside the key, so it never compares equal. No regex means this class of rule cannot fire here again. Mutation-checked, seven scenarios. Tolerated: whitespace around `=`, a leading indent, double-quoted YAML. Caught: the bare `skills/spring-docs/scripts/` prefix, and a commented-out setting in each of the three configs. --- .../generated-bundle-exclusions.test.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/__tests__/generated-bundle-exclusions.test.ts b/scripts/__tests__/generated-bundle-exclusions.test.ts index 362d514..e72d060 100644 --- a/scripts/__tests__/generated-bundle-exclusions.test.ts +++ b/scripts/__tests__/generated-bundle-exclusions.test.ts @@ -41,15 +41,19 @@ async function lines(file: string): Promise { /** * The patterns `.sonarcloud.properties` sets for `key`, as a `key=a,b` line. * - * `.properties` allows whitespace around the `=` and before the key, so match - * that rather than a bare prefix — a reformatted file should not fail the - * guard. A commented-out setting starts with `#`, which the anchor rejects. + * Split each line on its first `=` and compare the trimmed key, rather than + * matching a prefix: `.properties` allows whitespace around the separator and + * before the key, and a reformatted file should not fail the guard. A + * commented-out setting keeps its `#`, so its key never compares equal. */ async function sonarExclusions(key: string): Promise { - const setting = new RegExp(`^\\s*${key.replaceAll('.', '\\.')}\\s*=`) - const line = (await lines('.sonarcloud.properties')).find(l => setting.test(l)) - // The first `=` is the separator; no exclusion pattern contains one. - return line ? line.slice(line.indexOf('=') + 1).split(',').map(v => v.trim()).filter(Boolean) : [] + for (const line of await lines('.sonarcloud.properties')) { + const separator = line.indexOf('=') + if (separator < 0 || line.slice(0, separator).trim() !== key) + continue + return line.slice(separator + 1).split(',').map(v => v.trim()).filter(Boolean) + } + return [] } /** The `skills/` entries of `.codacy.yml`'s `exclude_paths:` block. */