Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface ReportViolationsOptions extends BaseHandlerOptions {
componentName: string;
groupBy?: 'file' | 'folder';
saveAsFile?: boolean;
excludePatterns?: string | string[];
}

export interface ViolationEntry {
Expand Down Expand Up @@ -45,6 +46,7 @@ export interface ReportAllViolationsOptions extends BaseHandlerOptions {
directory: string;
groupBy?: 'component' | 'file';
saveAsFile?: boolean;
excludePatterns?: string | string[];
}

export interface AllViolationsEntry {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const reportAllViolationsHandler = createHandler<
returnRawData: true,
directory: params.directory,
dsComponents,
excludePatterns: params.excludePatterns,
});

const raw = coverageResult.rawData?.rawPluginResult;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export const reportViolationsHandler = createHandler<
>(
reportViolationsSchema.name,
async (params, { cwd, workspaceRoot }) => {
const result = await analyzeViolationsBase<BaseViolationResult>(params);
const result = await analyzeViolationsBase<BaseViolationResult>({
cwd,
directory: params.directory,
componentName: params.componentName,
deprecatedCssClassesPath: params.deprecatedCssClassesPath,
excludePatterns: params.excludePatterns,
});

const failedAudits = filterFailedAudits(result);

if (failedAudits.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -71,6 +84,7 @@ export const createProjectAnalysisSchema = (
type: 'object',
properties: {
directory: COMMON_SCHEMA_PROPERTIES.directory,
excludePatterns: COMMON_SCHEMA_PROPERTIES.excludePatterns,
...additionalProperties,
},
required: ['directory'],
Expand All @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,39 @@ import { analyzeProjectCoverage as collectFilesViolations } from './coverage-ana
export async function analyzeViolationsBase<T extends BaseViolationResult>(
options: BaseViolationOptions,
): Promise<T> {
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,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, '<!DOUBLESTAR!>')
.replace(/\*/g, '[^/]*')
.replace(/<!DOUBLESTAR!>/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<FormattedCoverageResult> {
// Validate input parameters
if (!params.directory || typeof params.directory !== 'string') {
throw new Error('Directory parameter is required and must be a string');
}
Expand All @@ -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');
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface BaseViolationOptions {
directory: string;
componentName: string;
deprecatedCssClassesPath?: string;
excludePatterns?: string | string[];
}

export interface BaseViolationIssue {
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface ReportCoverageParams {
outputFormat?: 'text';
directory: string;
dsComponents: DsComponent[];
excludePatterns?: string | string[];
}

export interface DsComponent {
Expand Down
Loading