/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import * as fs from 'node:fs'; import * as path from 'node:path'; import ignore from 'ignore'; import { debugLogger } from './debugLogger.js'; import { getNormalizedRelativePath } from './ignorePathUtils.js'; export interface IgnoreFileFilter { isIgnored(filePath: string, isDirectory: boolean): boolean; getPatterns(): string[]; getIgnoreFilePaths(): string[]; hasPatterns(): boolean; } /** * An ignore file parser that reads the ignore files from the project root. */ export class IgnoreFileParser implements IgnoreFileFilter { private projectRoot: string; private patterns: string[] = []; private ig = ignore(); private readonly fileNames: string[]; constructor( projectRoot: string, // The order matters: files listed earlier have higher priority. // It can be a single file name/pattern or an array of file names/patterns. input: string | string[], isPatterns = false, ) { this.projectRoot = path.resolve(projectRoot); if (isPatterns) { this.fileNames = []; const patterns = Array.isArray(input) ? input : [input]; this.patterns.push(...patterns); this.ig.add(patterns); } else { this.fileNames = Array.isArray(input) ? input : [input]; this.loadPatternsFromFiles(); } } private loadPatternsFromFiles(): void { // Iterate in reverse order so that the first file in the list is processed last. // This gives the first file the highest priority, as patterns added later override earlier ones. for (const fileName of [...this.fileNames].reverse()) { const patterns = this.parseIgnoreFile(fileName); this.patterns.push(...patterns); this.ig.add(patterns); } } private parseIgnoreFile(fileName: string): string[] { const patternsFilePath = path.join(this.projectRoot, fileName); let content: string; try { content = fs.readFileSync(patternsFilePath, 'utf-8'); } catch { debugLogger.debug( `Ignore file not found: ${patternsFilePath}, continue without it.`, ); return []; } debugLogger.debug(`Loading ignore patterns from: ${patternsFilePath}`); return (content ?? '') .split(/\r\n|\n|\r/) .map((p) => p.trim()) .filter((p) => p !== '' && !p.startsWith('#')); } isIgnored(filePath: string, isDirectory: boolean): boolean { if (this.patterns.length === 0) { return false; } const normalizedPath = getNormalizedRelativePath( this.projectRoot, filePath, isDirectory, ); if ( normalizedPath === null || normalizedPath === '' || normalizedPath === '/' ) { return false; } return this.ig.ignores(normalizedPath); } getPatterns(): string[] { return this.patterns; } getIgnoreFilePaths(): string[] { return this.fileNames .slice() .reverse() .map((fileName) => path.join(this.projectRoot, fileName)) .filter( (filePath) => fs.statSync(filePath, { throwIfNoEntry: false })?.isFile() ?? false, ); } /** * Returns true if at least one ignore file exists and has patterns. */ hasPatterns(): boolean { return this.patterns.length > 0; } }