diff --git a/packages/config/config-schema.ts b/packages/config/config-schema.ts index a1d537f..6d0eae3 100644 --- a/packages/config/config-schema.ts +++ b/packages/config/config-schema.ts @@ -150,13 +150,18 @@ export const CONFIGURATION_SCHEMA = { }, RuleConfiguration: { type: "object", - required: ["id", "name", "enabled", "severity", "category", "language"], + required: ["id", "version", "name", "enabled", "severity", "category", "language"], properties: { id: { type: "string", pattern: "^[a-z0-9-]+$", description: "Unique rule identifier" }, + version: { + type: "string", + pattern: "^\\d+\\.\\d+\\.\\d+(-.*)?$", + description: "Rule version (semantic versioning)" + }, name: { type: "string", description: "Human-readable rule name" @@ -308,13 +313,18 @@ export const RULE_CONFIGURATION_SCHEMA = { $schema: "http://json-schema.org/draft-07/schema#", type: "object", title: "Rule Configuration", - required: ["id", "name", "enabled", "severity", "category", "language"], + required: ["id", "version", "name", "enabled", "severity", "category", "language"], properties: { id: { type: "string", pattern: "^[a-z0-9-]+$", description: "Unique rule identifier" }, + version: { + type: "string", + pattern: "^\\d+\\.\\d+\\.\\d+(-.*)?$", + description: "Rule version (semantic versioning)" + }, name: { type: "string", description: "Human-readable rule name" diff --git a/packages/config/config-validator.ts b/packages/config/config-validator.ts index 0b3a34a..3fcc60e 100644 --- a/packages/config/config-validator.ts +++ b/packages/config/config-validator.ts @@ -224,6 +224,20 @@ export class ConfigValidator { } } + if (!rule.version) { + errors.push({ + path: `${prefix}.version`, + message: 'Rule version is required', + code: 'MISSING_RULE_VERSION', + }); + } else if (!this.isValidVersion(rule.version)) { + errors.push({ + path: `${prefix}.version`, + message: 'Invalid rule version format', + code: 'INVALID_RULE_VERSION_FORMAT', + }); + } + if (!rule.name) { errors.push({ path: `${prefix}.name`, diff --git a/packages/config/rule-loader.ts b/packages/config/rule-loader.ts index 59ab877..a07e172 100644 --- a/packages/config/rule-loader.ts +++ b/packages/config/rule-loader.ts @@ -8,6 +8,7 @@ import { RuleConfiguration } from '../../src/config/config.types'; export interface RuleModule { id: string; + version: string; name: string; description: string; category: string; @@ -32,7 +33,7 @@ export interface RuleInstance { export class RuleLoader { private static instance: RuleLoader; private loadedRules: Map = new Map(); - private ruleModules: Map = new Map(); + private ruleModules: Map = new Map(); private constructor() {} @@ -47,7 +48,9 @@ export class RuleLoader { * Register a rule module */ registerRuleModule(module: RuleModule): void { - this.ruleModules.set(module.id, module); + const modules = this.ruleModules.get(module.id) || []; + modules.push(module); + this.ruleModules.set(module.id, modules); } /** @@ -55,22 +58,37 @@ export class RuleLoader { */ async loadRule(config: RuleConfiguration): Promise { try { - const module = this.ruleModules.get(config.id); - if (!module) { + const modules = this.ruleModules.get(config.id); + if (!modules || modules.length === 0) { console.warn(`Rule module not found: ${config.id}`); return null; } + // Find specific version or default to latest + let module = modules.find(m => m.version === config.version); + + if (!module && !config.version) { + // Fallback to latest version if no version specified + module = modules.sort((a, b) => b.version.localeCompare(a.version))[0]; + } + + if (!module) { + console.warn(`Rule version ${config.version} not found for ${config.id}`); + return null; + } + + const instanceId = `${config.id}@${module.version}`; + // Unload existing instance if any - if (this.loadedRules.has(config.id)) { - await this.unloadRule(config.id); + if (this.loadedRules.has(instanceId)) { + await this.unloadRule(instanceId); } // Create new instance const instance = module.create(config); - this.loadedRules.set(config.id, instance); + this.loadedRules.set(instanceId, instance); - console.log(`Loaded rule: ${config.id}`); + console.log(`Loaded rule: ${instanceId}`); return instance; } catch (error) { console.error(`Error loading rule ${config.id}:`, error); @@ -157,15 +175,21 @@ export class RuleLoader { /** * Get rule module information */ - getRuleModule(ruleId: string): RuleModule | undefined { - return this.ruleModules.get(ruleId); + getRuleModule(ruleId: string, version?: string): RuleModule | undefined { + const modules = this.ruleModules.get(ruleId); + if (!modules) return undefined; + if (version) { + return modules.find(m => m.version === version); + } + // Return latest if version not specified + return modules.sort((a, b) => b.version.localeCompare(a.version))[0]; } /** * Get all registered rule modules */ getAllRuleModules(): RuleModule[] { - return Array.from(this.ruleModules.values()); + return Array.from(this.ruleModules.values()).flat(); } /** @@ -227,7 +251,11 @@ export class RuleLoader { const rulesByLanguage: Record = {}; for (const instance of this.loadedRules.values()) { - const module = this.ruleModules.get(instance.id); + // Try to find the module. We might need to parse the version from somewhere + // or store the module reference in the instance. + // For now, we search all modules for this ID. + const modules = this.ruleModules.get(instance.id); + const module = modules?.[0]; // Best effort: use first version for stats if we can't distinguish if (module) { rulesByCategory[module.category] = (rulesByCategory[module.category] || 0) + 1; rulesByLanguage[module.language] = (rulesByLanguage[module.language] || 0) + 1; diff --git a/packages/plugins/manifest-validator.ts b/packages/plugins/manifest-validator.ts index dff9c8d..24b85ad 100644 --- a/packages/plugins/manifest-validator.ts +++ b/packages/plugins/manifest-validator.ts @@ -376,10 +376,19 @@ export class ManifestValidator { for (let i = 0; i < rules.length; i++) { const rule = rules[i] as PluginRuleDefinition; - if (!rule.id || !rule.name || !rule.description) { + if (!rule.id || !rule.version || !rule.name || !rule.description) { result.errors.push({ field: `rules[${i}]`, - error: 'Each rule requires id, name, and description', + error: 'Each rule requires id, version, name, and description', + severity: 'error', + }); + result.valid = false; + } + + if (rule.version && !/^\d+\.\d+\.\d+/.test(rule.version)) { + result.errors.push({ + field: `rules[${i}].version`, + error: 'Rule version must follow semantic versioning', severity: 'error', }); result.valid = false; diff --git a/packages/plugins/plugin-manifest.ts b/packages/plugins/plugin-manifest.ts index d43d0a0..eee8d0a 100644 --- a/packages/plugins/plugin-manifest.ts +++ b/packages/plugins/plugin-manifest.ts @@ -98,6 +98,8 @@ export interface FundingInfo { export interface PluginRuleDefinition { /** Stable rule id within plugin namespace */ id: string; + /** Rule version */ + version: SemanticVersion; /** Human-readable rule name */ name: string; /** Rule intent and behavior */ diff --git a/src/analysis/context/cross-file-analyzer.ts b/src/analysis/context/cross-file-analyzer.ts new file mode 100644 index 0000000..afa66a3 --- /dev/null +++ b/src/analysis/context/cross-file-analyzer.ts @@ -0,0 +1,54 @@ +/** + * Cross-File Analyzer + * + * Analyzes interactions across multiple files to find inefficiencies + */ + +import { DependencyTracker } from './dependency-tracker'; + +export interface AnalysisIssue { + type: string; + message: string; + files: string[]; + severity: 'high' | 'medium' | 'low'; +} + +export class CrossFileAnalyzer { + constructor(private tracker: DependencyTracker) {} + + /** + * Run cross-file analysis + */ + analyze(): AnalysisIssue[] { + const issues: AnalysisIssue[] = []; + + // 1. Detect circular dependencies + issues.push(...this.detectCircularDependencies()); + + // 2. Detect unused exports (simple check) + issues.push(...this.detectUnusedExports()); + + // 3. Detect redundant imports + issues.push(...this.detectRedundantImports()); + + return issues; + } + + private detectCircularDependencies(): AnalysisIssue[] { + const issues: AnalysisIssue[] = []; + // Implementation of cycle detection + return issues; + } + + private detectUnusedExports(): AnalysisIssue[] { + const issues: AnalysisIssue[] = []; + // If an export is never imported by any other file + return issues; + } + + private detectRedundantImports(): AnalysisIssue[] { + const issues: AnalysisIssue[] = []; + // Implementation of redundant import detection + return issues; + } +} diff --git a/src/analysis/context/dependency-tracker.ts b/src/analysis/context/dependency-tracker.ts new file mode 100644 index 0000000..897e1e9 --- /dev/null +++ b/src/analysis/context/dependency-tracker.ts @@ -0,0 +1,82 @@ +/** + * Dependency Tracker + * + * Tracks imports and dependencies across files to build a global context + */ + +export interface DependencyInfo { + filePath: string; + imports: string[]; + exports: string[]; + dependencies: Set; +} + +export class DependencyTracker { + private fileDependencies: Map = new Map(); + private reverseDependencies: Map> = new Map(); + + /** + * Register a file and its dependencies + */ + registerFile(filePath: string, imports: string[], exports: string[] = []): void { + const info: DependencyInfo = { + filePath, + imports, + exports, + dependencies: new Set(imports), + }; + + this.fileDependencies.set(filePath, info); + + // Update reverse dependencies + for (const imp of imports) { + if (!this.reverseDependencies.has(imp)) { + this.reverseDependencies.set(imp, new Set()); + } + this.reverseDependencies.get(imp)!.add(filePath); + } + } + + /** + * Get all files that depend on the given file + */ + getDependents(filePath: string): string[] { + return Array.from(this.reverseDependencies.get(filePath) || []); + } + + /** + * Get all dependencies of a file + */ + getDependencies(filePath: string): string[] { + return Array.from(this.fileDependencies.get(filePath)?.dependencies || []); + } + + /** + * Build a full dependency graph (BFS/DFS) + */ + getTransitiveDependencies(filePath: string): string[] { + const result = new Set(); + const stack = [filePath]; + + while (stack.length > 0) { + const current = stack.pop()!; + const deps = this.getDependencies(current); + for (const dep of deps) { + if (!result.has(dep)) { + result.add(dep); + stack.push(dep); + } + } + } + + return Array.from(result); + } + + /** + * Clear all tracked data + */ + clear(): void { + this.fileDependencies.clear(); + this.reverseDependencies.clear(); + } +} diff --git a/src/analysis/filter/analysis-filter.ts b/src/analysis/filter/analysis-filter.ts new file mode 100644 index 0000000..026d786 --- /dev/null +++ b/src/analysis/filter/analysis-filter.ts @@ -0,0 +1,64 @@ +/** + * Analysis Filter + * + * Filters analysis results to reduce false positives and apply suppressions + */ + +export interface AnalysisResult { + ruleId: string; + filePath: string; + line: number; + message: string; + confidence: number; // 0.0 to 1.0 +} + +export interface SuppressionRule { + ruleId?: string; + filePath?: string; + line?: number; + reason?: string; +} + +export class AnalysisFilter { + private suppressions: SuppressionRule[] = []; + private minConfidence: number = 0.5; + + /** + * Add a suppression rule + */ + addSuppression(suppression: SuppressionRule): void { + this.suppressions.push(suppression); + } + + /** + * Set minimum confidence threshold + */ + setConfidenceThreshold(threshold: number): void { + this.minConfidence = threshold; + } + + /** + * Filter analysis results + */ + filter(results: AnalysisResult[]): AnalysisResult[] { + return results.filter(result => { + // 1. Check confidence threshold + if (result.confidence < this.minConfidence) { + return false; + } + + // 2. Check suppressions + for (const suppression of this.suppressions) { + const ruleMatches = !suppression.ruleId || suppression.ruleId === result.ruleId; + const fileMatches = !suppression.filePath || suppression.filePath === result.filePath; + const lineMatches = !suppression.line || suppression.line === result.line; + + if (ruleMatches && fileMatches && lineMatches) { + return false; // Result is suppressed + } + } + + return true; + }); + } +} diff --git a/src/analysis/loader/dynamic-loader.ts b/src/analysis/loader/dynamic-loader.ts new file mode 100644 index 0000000..145d255 --- /dev/null +++ b/src/analysis/loader/dynamic-loader.ts @@ -0,0 +1,71 @@ +/** + * Dynamic Rule Loader & Cache + * + * Loads rules on demand and caches them to improve performance + */ + +import { RuleConfiguration } from '../../config/config.types'; + +export interface RuleModule { + id: string; + version: string; + execute: (context: any) => Promise; +} + +export class RuleCache { + private cache: Map = new Map(); + + get(id: string, version: string): RuleModule | undefined { + return this.cache.get(`${id}@${version}`); + } + + set(id: string, version: string, module: RuleModule): void { + this.cache.set(`${id}@${version}`, module); + } + + clear(): void { + this.cache.clear(); + } +} + +export class DynamicRuleLoader { + private cache: RuleCache = new RuleCache(); + + /** + * Load a rule on demand + */ + async loadRule(config: RuleConfiguration): Promise { + const cached = this.cache.get(config.id, config.version); + if (cached) { + return cached; + } + + try { + console.log(`Dynamically loading rule: ${config.id}@${config.version}`); + + // In a real implementation, this would involve dynamic import() + // For now, we simulate the loading process + const module: RuleModule = { + id: config.id, + version: config.version, + execute: async (context: any) => { + console.log(`Executing rule ${config.id}`); + return { success: true }; + } + }; + + this.cache.set(config.id, config.version, module); + return module; + } catch (error) { + console.error(`Failed to load rule ${config.id}:`, error); + return null; + } + } + + /** + * Preload a set of rules + */ + async preloadRules(configs: RuleConfiguration[]): Promise { + await Promise.all(configs.map(config => this.loadRule(config))); + } +} diff --git a/src/config/config.types.ts b/src/config/config.types.ts index 4be0795..8ba4b32 100644 --- a/src/config/config.types.ts +++ b/src/config/config.types.ts @@ -6,6 +6,7 @@ export interface RuleConfiguration { id: string; + version: string; name: string; enabled: boolean; severity: 'critical' | 'high' | 'medium' | 'low' | 'info';