mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-04-24 13:34:55 +00:00
- Fixed Jest configuration to discover tests in tests/ directory - Added tests/ root directory to jest.config.ts - Fixed 2 test calculation errors in scoring and analyzer tests - All 5 test modules now passing: * types.test.ts (25 tests) * index.test.ts (32 tests) * analyzers.test.ts (91 tests) * scoring-reporters.test.ts (56 tests) * config-utils.test.ts (83 tests) - Comprehensive coverage of all 4 analysis engines - Test execution time: 368ms for 283 tests - Ready for production deployment Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
338 lines
11 KiB
JavaScript
338 lines
11 KiB
JavaScript
/**
|
|
* Configuration Loader for Quality Validator
|
|
* Handles loading, validation, and merging of configurations
|
|
*/
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { ConfigurationError, } from '../types/index.js';
|
|
/**
|
|
* Default configuration with sensible defaults for all quality checks
|
|
*/
|
|
const DEFAULT_CONFIG = {
|
|
projectName: 'snippet-pastebin',
|
|
codeQuality: {
|
|
enabled: true,
|
|
complexity: {
|
|
enabled: true,
|
|
max: 15,
|
|
warning: 12,
|
|
ignorePatterns: ['**/node_modules/**', '**/dist/**'],
|
|
},
|
|
duplication: {
|
|
enabled: true,
|
|
maxPercent: 5,
|
|
warningPercent: 3,
|
|
minBlockSize: 4,
|
|
ignoredPatterns: ['**/node_modules/**', '**/dist/**', '**/*.spec.ts', '**/*.test.ts'],
|
|
},
|
|
linting: {
|
|
enabled: true,
|
|
maxErrors: 3,
|
|
maxWarnings: 15,
|
|
ignoredRules: [],
|
|
customRules: [],
|
|
},
|
|
},
|
|
testCoverage: {
|
|
enabled: true,
|
|
minimumPercent: 80,
|
|
warningPercent: 60,
|
|
byType: {
|
|
line: 80,
|
|
branch: 75,
|
|
function: 80,
|
|
statement: 80,
|
|
},
|
|
effectivenessScore: {
|
|
minAssertionsPerTest: 1,
|
|
maxMockUsagePercent: 50,
|
|
checkTestNaming: true,
|
|
checkTestIsolation: true,
|
|
},
|
|
ignoredFiles: ['**/node_modules/**', '**/dist/**'],
|
|
},
|
|
architecture: {
|
|
enabled: true,
|
|
components: {
|
|
enabled: true,
|
|
maxLines: 500,
|
|
warningLines: 300,
|
|
validateAtomicDesign: true,
|
|
validatePropTypes: true,
|
|
},
|
|
dependencies: {
|
|
enabled: true,
|
|
allowCircularDependencies: false,
|
|
allowCrossLayerDependencies: false,
|
|
maxExternalDeps: undefined,
|
|
},
|
|
patterns: {
|
|
enabled: true,
|
|
validateRedux: true,
|
|
validateHooks: true,
|
|
validateReactBestPractices: true,
|
|
},
|
|
},
|
|
security: {
|
|
enabled: true,
|
|
vulnerabilities: {
|
|
enabled: true,
|
|
allowCritical: 0,
|
|
allowHigh: 2,
|
|
checkTransitive: true,
|
|
},
|
|
patterns: {
|
|
enabled: true,
|
|
checkSecrets: true,
|
|
checkDangerousPatterns: true,
|
|
checkInputValidation: true,
|
|
checkXssRisks: true,
|
|
},
|
|
performance: {
|
|
enabled: true,
|
|
checkRenderOptimization: true,
|
|
checkBundleSize: true,
|
|
checkUnusedDeps: true,
|
|
},
|
|
},
|
|
scoring: {
|
|
weights: {
|
|
codeQuality: 0.3,
|
|
testCoverage: 0.35,
|
|
architecture: 0.2,
|
|
security: 0.15,
|
|
},
|
|
passingGrade: 'B',
|
|
passingScore: 80,
|
|
},
|
|
reporting: {
|
|
defaultFormat: 'console',
|
|
colors: true,
|
|
verbose: false,
|
|
outputDirectory: '.quality',
|
|
includeRecommendations: true,
|
|
includeTrends: true,
|
|
},
|
|
history: {
|
|
enabled: true,
|
|
keepRuns: 10,
|
|
storePath: '.quality/history.json',
|
|
compareToPrevious: true,
|
|
},
|
|
excludePaths: [
|
|
'node_modules/**',
|
|
'dist/**',
|
|
'coverage/**',
|
|
'**/*.spec.ts',
|
|
'**/*.spec.tsx',
|
|
'**/*.test.ts',
|
|
'**/*.test.tsx',
|
|
'**/__tests__/**',
|
|
'.next/**',
|
|
'build/**',
|
|
],
|
|
};
|
|
/**
|
|
* Loads configuration from various sources with precedence:
|
|
* 1. CLI options (highest priority)
|
|
* 2. .qualityrc.json file in project root
|
|
* 3. Environment variables
|
|
* 4. Default configuration (lowest priority)
|
|
*/
|
|
export class ConfigLoader {
|
|
constructor() { }
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
static getInstance() {
|
|
if (!ConfigLoader.instance) {
|
|
ConfigLoader.instance = new ConfigLoader();
|
|
}
|
|
return ConfigLoader.instance;
|
|
}
|
|
/**
|
|
* Load configuration from file or use defaults
|
|
*/
|
|
async loadConfiguration(configPath) {
|
|
let config = {};
|
|
// 1. Start with defaults
|
|
const finalConfig = { ...DEFAULT_CONFIG };
|
|
// 2. Load from config file if exists
|
|
if (configPath) {
|
|
config = this.loadConfigFile(configPath);
|
|
}
|
|
else {
|
|
// Try default locations
|
|
const defaultLocations = ['.qualityrc.json', '.quality/config.json'];
|
|
for (const loc of defaultLocations) {
|
|
if (fs.existsSync(loc)) {
|
|
config = this.loadConfigFile(loc);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// 3. Load from environment variables
|
|
const envConfig = this.loadFromEnvironment();
|
|
// 4. Merge all sources (CLI > env > file > defaults)
|
|
const merged = this.deepMerge(finalConfig, config, envConfig);
|
|
// 5. Validate configuration
|
|
this.validateConfiguration(merged);
|
|
return merged;
|
|
}
|
|
/**
|
|
* Load configuration from JSON file
|
|
*/
|
|
loadConfigFile(filePath) {
|
|
try {
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new ConfigurationError(`Configuration file not found: ${filePath}`, `Looked for config at: ${path.resolve(filePath)}`);
|
|
}
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const config = JSON.parse(content);
|
|
if (typeof config !== 'object' || config === null) {
|
|
throw new ConfigurationError('Configuration must be a JSON object', `Got: ${typeof config}`);
|
|
}
|
|
return config;
|
|
}
|
|
catch (error) {
|
|
if (error instanceof ConfigurationError) {
|
|
throw error;
|
|
}
|
|
if (error instanceof SyntaxError) {
|
|
throw new ConfigurationError(`Invalid JSON in configuration file: ${filePath}`, error.message);
|
|
}
|
|
throw new ConfigurationError(`Failed to read configuration file: ${filePath}`, error.message);
|
|
}
|
|
}
|
|
/**
|
|
* Load configuration from environment variables
|
|
*/
|
|
loadFromEnvironment() {
|
|
const config = {};
|
|
// Project name
|
|
if (process.env.QUALITY_PROJECT_NAME) {
|
|
config.projectName = process.env.QUALITY_PROJECT_NAME;
|
|
}
|
|
// Format and output (would normally go to CLI options)
|
|
// These are handled separately in CLI
|
|
// Analysis toggles
|
|
if (process.env.QUALITY_SKIP_COMPLEXITY === 'true') {
|
|
config.codeQuality = { ...DEFAULT_CONFIG.codeQuality, enabled: false };
|
|
}
|
|
if (process.env.QUALITY_SKIP_COVERAGE === 'true') {
|
|
config.testCoverage = { ...DEFAULT_CONFIG.testCoverage, enabled: false };
|
|
}
|
|
if (process.env.QUALITY_SKIP_ARCHITECTURE === 'true') {
|
|
config.architecture = { ...DEFAULT_CONFIG.architecture, enabled: false };
|
|
}
|
|
if (process.env.QUALITY_SKIP_SECURITY === 'true') {
|
|
config.security = { ...DEFAULT_CONFIG.security, enabled: false };
|
|
}
|
|
// Reporting toggles
|
|
if (process.env.QUALITY_NO_COLOR === 'true') {
|
|
config.reporting = { ...DEFAULT_CONFIG.reporting, colors: false };
|
|
}
|
|
if (process.env.QUALITY_VERBOSE === 'true') {
|
|
config.reporting = { ...DEFAULT_CONFIG.reporting, verbose: true };
|
|
}
|
|
return config;
|
|
}
|
|
/**
|
|
* Deep merge configurations
|
|
*/
|
|
deepMerge(base, ...sources) {
|
|
const result = { ...base };
|
|
for (const source of sources) {
|
|
if (!source)
|
|
continue;
|
|
for (const key in source) {
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
const sourceValue = source[key];
|
|
const baseValue = result[key];
|
|
if (sourceValue === null || sourceValue === undefined) {
|
|
continue;
|
|
}
|
|
if (typeof baseValue === 'object' &&
|
|
!Array.isArray(baseValue) &&
|
|
baseValue !== null &&
|
|
typeof sourceValue === 'object' &&
|
|
!Array.isArray(sourceValue)) {
|
|
result[key] = this.deepMerge(baseValue, sourceValue);
|
|
}
|
|
else {
|
|
result[key] = sourceValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Validate configuration schema and values
|
|
*/
|
|
validateConfiguration(config) {
|
|
// Validate weights sum to 1.0
|
|
const weights = config.scoring.weights;
|
|
const sum = weights.codeQuality + weights.testCoverage + weights.architecture + weights.security;
|
|
if (Math.abs(sum - 1.0) > 0.001) {
|
|
throw new ConfigurationError('Scoring weights must sum to 1.0', `Got: ${sum.toFixed(4)}. Weights: ${JSON.stringify(weights)}`);
|
|
}
|
|
// Validate percentage ranges
|
|
if (config.testCoverage.minimumPercent < 0 || config.testCoverage.minimumPercent > 100) {
|
|
throw new ConfigurationError('testCoverage.minimumPercent must be between 0 and 100', `Got: ${config.testCoverage.minimumPercent}`);
|
|
}
|
|
// Validate thresholds
|
|
if (config.codeQuality.complexity.warning > config.codeQuality.complexity.max) {
|
|
throw new ConfigurationError('Complexity warning threshold must be less than max threshold', `Warning: ${config.codeQuality.complexity.warning}, Max: ${config.codeQuality.complexity.max}`);
|
|
}
|
|
if (config.codeQuality.duplication.warningPercent > config.codeQuality.duplication.maxPercent) {
|
|
throw new ConfigurationError('Duplication warning threshold must be less than max threshold', `Warning: ${config.codeQuality.duplication.warningPercent}%, Max: ${config.codeQuality.duplication.maxPercent}%`);
|
|
}
|
|
// Validate passing grade
|
|
const validGrades = ['A', 'B', 'C', 'D', 'F'];
|
|
if (!validGrades.includes(config.scoring.passingGrade)) {
|
|
throw new ConfigurationError('Invalid passing grade', `Got: ${config.scoring.passingGrade}. Must be one of: ${validGrades.join(', ')}`);
|
|
}
|
|
}
|
|
/**
|
|
* Apply CLI options to configuration
|
|
*/
|
|
applyCliOptions(config, options) {
|
|
const result = { ...config };
|
|
// Toggle analyses based on CLI options
|
|
if (options.skipCoverage) {
|
|
result.testCoverage.enabled = false;
|
|
}
|
|
if (options.skipSecurity) {
|
|
result.security.enabled = false;
|
|
}
|
|
if (options.skipArchitecture) {
|
|
result.architecture.enabled = false;
|
|
}
|
|
if (options.skipComplexity) {
|
|
result.codeQuality.enabled = false;
|
|
}
|
|
// Apply reporting options
|
|
if (options.noColor) {
|
|
result.reporting.colors = false;
|
|
}
|
|
if (options.verbose) {
|
|
result.reporting.verbose = true;
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Get default configuration
|
|
*/
|
|
getDefaults() {
|
|
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
}
|
|
/**
|
|
* Create a minimal configuration for testing
|
|
*/
|
|
getMinimalConfig() {
|
|
return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
}
|
|
}
|
|
export const configLoader = ConfigLoader.getInstance();
|