diff --git a/scripts/hooks/config-protection.js b/scripts/hooks/config-protection.js index 6e9350d4d..75a7a0781 100644 --- a/scripts/hooks/config-protection.js +++ b/scripts/hooks/config-protection.js @@ -43,9 +43,12 @@ const PROTECTED_FILES = new Set([ 'prettier.config.js', 'prettier.config.cjs', 'prettier.config.mjs', - // Biome + // Biome's discovered filenames. Custom --config-path/extends targets need + // reference context; an arbitrary biome.* basename is not sufficient. 'biome.json', 'biome.jsonc', + '.biome.json', + '.biome.jsonc', // Ruff (Python) '.ruff.toml', 'ruff.toml', @@ -113,11 +116,9 @@ const PROTECTED_FILES = new Set([ */ const PROTECTED_PATTERNS = [ // eslint.config.base.mjs, prettier.config.shared.cjs, stylelint.config.local.js ... - /^(eslint|prettier|stylelint|commitlint|oxlint|biome)\.config(\.[A-Za-z0-9_-]+)*\.(js|mjs|cjs|ts|mts|cts)$/i, + /^(eslint|prettier|stylelint|commitlint|oxlint)\.config(\.[A-Za-z0-9_-]+)*\.(js|mjs|cjs|ts|mts|cts)$/i, // .eslintrc.base.json, .prettierrc.shared.yml ... /^\.(eslintrc|prettierrc|stylelintrc|markdownlintrc)(\.[A-Za-z0-9_-]+)*\.(js|cjs|mjs|json|jsonc|yml|yaml|toml)$/i, - // biome.base.json, biome.shared.jsonc - /^biome(\.[A-Za-z0-9_-]+)*\.jsonc?$/i, ]; function isProtectedName(basename) { diff --git a/tests/hooks/config-protection.test.js b/tests/hooks/config-protection.test.js index 2ebedd6e7..b0b88ae54 100644 --- a/tests/hooks/config-protection.test.js +++ b/tests/hooks/config-protection.test.js @@ -428,8 +428,8 @@ function runTests() { const names = [ 'eslint.config.base.mjs', 'prettier.config.shared.cjs', '.eslintrc.base.json', 'ESLint.Config.Base.MJS', 'stylelint.config.local.ts', 'commitlint.config.shared.cts', 'oxlint.config.base.mts', - 'biome.config.shared.js', '.prettierrc.shared.yml', '.stylelintrc.team.toml', - '.markdownlintrc.team.jsonc', 'biome.shared.jsonc', 'BIOME.Team.Base.JSON' + '.prettierrc.shared.yml', '.stylelintrc.team.toml', + '.markdownlintrc.team.jsonc' ]; for (const name of names) { const absPath = path.join(tmpDir, name); @@ -476,6 +476,34 @@ function runTests() { }) ); + results.push( + test('Biome filenames protect discovered configs without blocking ordinary result files', () => { + return withOwnedDirectory(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-biome-')), tmpDir => { + // Arbitrary --config-path/extends targets need reference context; their + // basename alone does not prove that a file is Biome configuration. + const cases = [ + ['biome.results.json', 0], ['biome.report.jsonc', 0], + ['biome.json', 2], ['biome.jsonc', 2], ['.biome.json', 2], ['.biome.jsonc', 2], + ['BIOME.JSON', 2], ['.BIOME.JSONC', 2], + ['biome.shared.jsonc', 0], + ['BIOME.Team.Base.JSON', 0], ['biome.config.shared.js', 0], ['biome.json.bak', 0], + ]; + for (const [name, expected] of cases) { + const absPath = path.join(tmpDir, name); + const input = { tool_name: 'Write', tool_input: { file_path: absPath, content: '{}' } }; + // Start each spelling independently on case-insensitive filesystems. + fs.rmSync(absPath, { force: true }); + assert.strictEqual(runHook(input).code, 0, 'First creation should be allowed: ' + name); + fs.writeFileSync(absPath, '{}'); + const result = runHook(input); + assert.strictEqual(result.code, expected, 'Unexpected filename classification: ' + name); + assert.strictEqual(result.stdout, '', 'No raw input should be echoed: ' + name); + assert.strictEqual(fs.readFileSync(absPath, 'utf8'), '{}', 'Hook must not modify the fixture'); + } + }); + }) + ); + const passed = results.filter(result => result === 'passed').length; const failed = results.filter(result => result === 'failed').length; const skipped = results.filter(result => result === 'skipped').length;