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
14 changes: 12 additions & 2 deletions packages/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 14 additions & 0 deletions packages/config/config-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
52 changes: 40 additions & 12 deletions packages/config/rule-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RuleConfiguration } from '../../src/config/config.types';

export interface RuleModule {
id: string;
version: string;
name: string;
description: string;
category: string;
Expand All @@ -32,7 +33,7 @@ export interface RuleInstance {
export class RuleLoader {
private static instance: RuleLoader;
private loadedRules: Map<string, RuleInstance> = new Map();
private ruleModules: Map<string, RuleModule> = new Map();
private ruleModules: Map<string, RuleModule[]> = new Map();

private constructor() {}

Expand All @@ -47,30 +48,47 @@ 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);
}

/**
* Load a rule based on configuration
*/
async loadRule(config: RuleConfiguration): Promise<RuleInstance | null> {
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);
Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -227,7 +251,11 @@ export class RuleLoader {
const rulesByLanguage: Record<string, number> = {};

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;
Expand Down
13 changes: 11 additions & 2 deletions packages/plugins/manifest-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/plugin-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
54 changes: 54 additions & 0 deletions src/analysis/context/cross-file-analyzer.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
82 changes: 82 additions & 0 deletions src/analysis/context/dependency-tracker.ts
Original file line number Diff line number Diff line change
@@ -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<string>;
}

export class DependencyTracker {
private fileDependencies: Map<string, DependencyInfo> = new Map();
private reverseDependencies: Map<string, Set<string>> = 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<string>();
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();
}
}
Loading
Loading