diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts index d20bf21..12893cd 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/models/types.ts @@ -9,6 +9,7 @@ export interface ReportViolationsOptions extends BaseHandlerOptions { componentName: string; groupBy?: 'file' | 'folder'; saveAsFile?: boolean; + excludePatterns?: string | string[]; } export interface ViolationEntry { @@ -45,6 +46,7 @@ export interface ReportAllViolationsOptions extends BaseHandlerOptions { directory: string; groupBy?: 'component' | 'file'; saveAsFile?: boolean; + excludePatterns?: string | string[]; } export interface AllViolationsEntry { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts index ce168ca..c977cd6 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-all-violations.tool.ts @@ -91,6 +91,7 @@ export const reportAllViolationsHandler = createHandler< returnRawData: true, directory: params.directory, dsComponents, + excludePatterns: params.excludePatterns, }); const raw = coverageResult.rawData?.rawPluginResult; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts index bc456eb..1f11a48 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/report-violations/report-violations.tool.ts @@ -30,7 +30,14 @@ export const reportViolationsHandler = createHandler< >( reportViolationsSchema.name, async (params, { cwd, workspaceRoot }) => { - const result = await analyzeViolationsBase(params); + const result = await analyzeViolationsBase({ + cwd, + directory: params.directory, + componentName: params.componentName, + deprecatedCssClassesPath: params.deprecatedCssClassesPath, + excludePatterns: params.excludePatterns, + }); + const failedAudits = filterFailedAudits(result); if (failedAudits.length === 0) { diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts index 774b4e6..65d5dbe 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/models/schema-helpers.ts @@ -24,6 +24,19 @@ export const COMMON_SCHEMA_PROPERTIES = { 'How to group the violation results in the output. "file" groups violations by individual file paths, "folder" groups by directory structure.', default: 'file' as const, }, + + excludePatterns: { + anyOf: [ + { type: 'string' as const }, + { type: 'array' as const, items: { type: 'string' as const } }, + ], + description: + 'Glob pattern(s) to exclude files/directories from scanning. ' + + 'Supports standard glob syntax: * (any chars except /), ** (any chars including /), ' + + '? (single char). ' + + 'Examples: "node_modules/**", "**/dist/**", "**/*.spec.ts". ' + + 'Can be a single string or array of strings.', + }, } as const; /** @@ -71,6 +84,7 @@ export const createProjectAnalysisSchema = ( type: 'object', properties: { directory: COMMON_SCHEMA_PROPERTIES.directory, + excludePatterns: COMMON_SCHEMA_PROPERTIES.excludePatterns, ...additionalProperties, }, required: ['directory'], @@ -87,6 +101,7 @@ export const createViolationReportingSchema = ( directory: COMMON_SCHEMA_PROPERTIES.directory, componentName: COMMON_SCHEMA_PROPERTIES.componentName, groupBy: COMMON_SCHEMA_PROPERTIES.groupBy, + excludePatterns: COMMON_SCHEMA_PROPERTIES.excludePatterns, ...additionalProperties, }, required: ['directory', 'componentName'], diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts index 08d17cb..fb91a47 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/base-analyzer.ts @@ -14,44 +14,39 @@ import { analyzeProjectCoverage as collectFilesViolations } from './coverage-ana export async function analyzeViolationsBase( options: BaseViolationOptions, ): Promise { - const { - cwd = process.cwd(), - directory, - componentName, - deprecatedCssClassesPath, - } = options; + const cwd = options.cwd || process.cwd(); - validateComponentName(componentName); + validateComponentName(options.componentName); - if (!directory || typeof directory !== 'string') { + if (!options.directory || typeof options.directory !== 'string') { throw new Error('Directory parameter is required and must be a string'); } process.chdir(cwd); - if (!deprecatedCssClassesPath) { + if (!options.deprecatedCssClassesPath) { throw new Error( 'Missing ds.deprecatedCssClassesPath. Provide --ds.deprecatedCssClassesPath in mcp.json file.', ); } const deprecatedCssClasses = await getDeprecatedCssClasses( - componentName, - deprecatedCssClassesPath, + options.componentName, + options.deprecatedCssClassesPath, cwd, ); const dsComponents = [ { - componentName, + componentName: options.componentName, deprecatedCssClasses, }, ]; const params: ReportCoverageParams = { + ...options, cwd, returnRawData: true, - directory, dsComponents, }; diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts index 0aa9bd2..150146b 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/coverage-analyzer.ts @@ -7,6 +7,7 @@ import { FormattedCoverageResult, } from './types.js'; import { resolveCrossPlatformPath } from '../utils/cross-platform-path.js'; +import { normalizeFilePath } from './formatters.js'; /** * Extracts component name from audit title - performance optimized with caching @@ -25,13 +26,82 @@ export function extractComponentName(title: string): string { return componentName; } +/** + * Normalizes excludePatterns to an array format + */ +function normalizeExcludePatterns( + patterns: string | string[] | undefined, +): string[] { + if (!patterns) { + return []; + } + return Array.isArray(patterns) ? patterns : [patterns]; +} + +/** + * Converts a glob pattern to a regular expression. + * Supports: *, **, ? + */ +function globToRegex(pattern: string): RegExp { + let regexPattern = pattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\?/g, '[^/]') + .replace(/\*\*/g, '') + .replace(/\*/g, '[^/]*') + .replace(//g, '.*'); + + if (pattern.startsWith('**/')) { + regexPattern = regexPattern.replace(/^\.\*\//, ''); + regexPattern = `^(?:.*\\/)?${regexPattern}`; + } else { + regexPattern = `^${regexPattern}`; + } + + if (!regexPattern.endsWith('$')) { + regexPattern = `${regexPattern}$`; + } + + return new RegExp(regexPattern); +} + +function validateGlobPattern(pattern: string): void { + try { + globToRegex(pattern); + } catch (ctx) { + throw new Error( + `Invalid glob pattern "${pattern}": ${ctx instanceof Error ? ctx.message : 'Unknown error'}`, + ); + } +} + +function validateExcludePatterns( + patterns: string | string[] | undefined, +): void { + const normalized = normalizeExcludePatterns(patterns); + for (const pattern of normalized) { + validateGlobPattern(pattern); + } +} + +function shouldExcludeFile( + filePath: string, + excludeRegexes: RegExp[], +): boolean { + if (excludeRegexes.length === 0) { + return false; + } + + const normalizedPath = filePath.replace(/\\/g, '/'); + + return excludeRegexes.some((regex) => regex.test(normalizedPath)); +} + /** * Main implementation function for reporting project coverage */ export async function analyzeProjectCoverage( params: ReportCoverageParams, ): Promise { - // Validate input parameters if (!params.directory || typeof params.directory !== 'string') { throw new Error('Directory parameter is required and must be a string'); } @@ -44,17 +114,25 @@ export async function analyzeProjectCoverage( ); } + if (params.excludePatterns) { + validateExcludePatterns(params.excludePatterns); + } + + const normalizedPatterns = normalizeExcludePatterns(params.excludePatterns); + const excludeRegexes = normalizedPatterns.map(globToRegex); + if (params.cwd) { process.chdir(params.cwd); } - // Execute the coverage plugin + const scanRootDirectory = resolveCrossPlatformPath( + params.cwd || process.cwd(), + params.directory, + ); + const pluginConfig = await dsComponentCoveragePlugin({ dsComponents: params.dsComponents, - directory: resolveCrossPlatformPath( - params.cwd || process.cwd(), - params.directory, - ), + directory: scanRootDirectory, }); const { executePlugin } = await import('@code-pushup/core'); @@ -63,13 +141,46 @@ export async function analyzeProjectCoverage( persist: { outputDir: '' }, })) as BaseViolationResult; + const filteredResult: BaseViolationResult = { + ...result, + audits: result.audits.map((audit) => { + if (!audit.details?.issues) { + return audit; + } + + const filteredIssues = audit.details.issues.filter((issue) => { + if (!issue.source?.file) { + return true; + } + + const relativePath = normalizeFilePath( + issue.source.file, + params.directory, + ); + + return !shouldExcludeFile(relativePath, excludeRegexes); + }); + + const score = filteredIssues.length === 0 ? 1 : audit.score; + + return { + ...audit, + score, + details: { + ...audit.details, + issues: filteredIssues, + }, + }; + }), + }; + const formattedResult: FormattedCoverageResult = { - textOutput: '', // No longer used, kept for backwards compatibility + textOutput: '', }; if (params.returnRawData) { formattedResult.rawData = { - rawPluginResult: result, + rawPluginResult: filteredResult, pluginOptions: { directory: params.directory, dsComponents: params.dsComponents, diff --git a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/types.ts b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/types.ts index c8d1fe5..f564ef6 100644 --- a/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/types.ts +++ b/packages/angular-mcp-server/src/lib/tools/ds/shared/violation-analysis/types.ts @@ -7,6 +7,7 @@ export interface BaseViolationOptions { directory: string; componentName: string; deprecatedCssClassesPath?: string; + excludePatterns?: string | string[]; } export interface BaseViolationIssue { @@ -39,6 +40,7 @@ export interface ReportCoverageParams { outputFormat?: 'text'; directory: string; dsComponents: DsComponent[]; + excludePatterns?: string | string[]; } export interface DsComponent {