import type { Condition } from '../rules/Condition'; import type { RuleDefinition } from '../rules/RuleDefinition'; import type { Fact } from '../facts/Fact'; import type { DependencyFact } from '../facts/DependencyFact'; import type { FileChangeFact } from '../facts/FileChangeFact'; import type { FileContentFact } from '../facts/FileContentFact'; import { matchesScope } from './scopeMatcher'; type RuleScope = RuleDefinition['scope']; type FactInput = Fact | FileChangeFact | FileContentFact; type FactMatch = { facts: ReadonlyArray; scope?: RuleScope; }; const isFileChangeFact = (fact: FactInput): fact is FileChangeFact => fact.kind === 'FileChange'; const isFileContentFact = (fact: FactInput): fact is FileContentFact => fact.kind === 'FileContent'; const isDependencyFact = (fact: FactInput): fact is DependencyFact => fact.kind === 'Dependency'; const isHeuristicFact = (fact: FactInput): fact is Extract => fact.kind === 'Heuristic'; const matchesFileChange = ( condition: Extract, facts: ReadonlyArray ): boolean => { for (const fact of facts) { if (!isFileChangeFact(fact)) { continue; } if (condition.where?.pathPrefix && !fact.path.startsWith(condition.where.pathPrefix)) { continue; } if (condition.where?.changeType && fact.changeType !== condition.where.changeType) { continue; } return true; } return false; }; const matchesDependency = ( condition: Extract, facts: ReadonlyArray ): boolean => { for (const fact of facts) { if (!isDependencyFact(fact)) { continue; } if (condition.where?.from && fact.from !== condition.where.from) { continue; } if (condition.where?.to && fact.to !== condition.where.to) { continue; } return true; } return false; }; const matchesFileContent = ( condition: Extract, match: FactMatch ): boolean => { const contains = condition.contains; const regex = condition.regex; for (const fact of match.facts) { if (!isFileContentFact(fact)) { continue; } if (!matchesScope(fact.path, match.scope)) { continue; } if (contains && contains.length > 0 && !contains.every((token) => fact.content.includes(token))) { continue; } if (regex && regex.length > 0) { const matchesAll = regex.every((pattern) => new RegExp(pattern).test(fact.content)); if (!matchesAll) { continue; } } return true; } return false; }; const matchesHeuristic = ( condition: Extract, facts: ReadonlyArray ): boolean => { for (const fact of facts) { if (!isHeuristicFact(fact)) { continue; } if (condition.where?.ruleId && fact.ruleId !== condition.where.ruleId) { continue; } if (condition.where?.code && fact.code !== condition.where.code) { continue; } if (condition.where?.filePathPrefix) { const filePath = fact.filePath ?? ''; if (!filePath.startsWith(condition.where.filePathPrefix)) { continue; } } return true; } return false; }; export const conditionMatches = ( condition: Condition, facts: ReadonlyArray, scope?: RuleScope ): boolean => { const match: FactMatch = { facts, scope }; if (condition.kind === 'FileChange') { return matchesFileChange(condition, facts); } if (condition.kind === 'FileContent') { return matchesFileContent(condition, match); } if (condition.kind === 'Dependency') { return matchesDependency(condition, facts); } if (condition.kind === 'Heuristic') { return matchesHeuristic(condition, facts); } if (condition.kind === 'All') { return condition.conditions.every((child) => conditionMatches(child, facts, scope)); } if (condition.kind === 'Any') { return condition.conditions.some((child) => conditionMatches(child, facts, scope)); } return !conditionMatches(condition.condition, facts, scope); };