From ec332e372154b7ada9a74f0bdcbe41937c02c78c Mon Sep 17 00:00:00 2001 From: Nabeelahh Date: Wed, 27 May 2026 11:25:31 +0100 Subject: [PATCH] rule conflict fetection --- .github/workflows/ci.yml | 115 +---- .../conflicts/conflict-detector.spec.ts | 442 ++++++++++++++++++ src/analysis/conflicts/conflict-detector.ts | 437 +++++++++++++++++ src/analysis/conflicts/conflict-warned.ts | 242 ++++++++++ src/analysis/conflicts/index.ts | 9 + src/analysis/conflicts/types.ts | 109 +++++ 6 files changed, 1246 insertions(+), 108 deletions(-) create mode 100644 src/analysis/conflicts/conflict-detector.spec.ts create mode 100644 src/analysis/conflicts/conflict-detector.ts create mode 100644 src/analysis/conflicts/conflict-warned.ts create mode 100644 src/analysis/conflicts/index.ts create mode 100644 src/analysis/conflicts/types.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e774bbc..26ff3a1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,3 @@ - coverage-report: - name: Upload Coverage to Codecov - runs-on: ubuntu-latest - needs: [node-lint, node-build] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - name: Install pnpm - run: npm install -g pnpm - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Run tests with coverage - run: pnpm test -- --coverage || npm test -- --coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: ./coverage/lcov.info - flags: unittests - name: codecov-umbrella - continue-on-error: true name: CI on: @@ -90,15 +65,6 @@ jobs: node-version: '20' cache: 'pnpm' - - name: Detect package manager - id: package-manager - run: | - if [ -f "pnpm-lock.yaml" ]; then - echo "manager=pnpm" >> $GITHUB_OUTPUT - echo "lockfile=pnpm-lock.yaml" >> $GITHUB_OUTPUT - elif [ -f "yarn.lock" ]; then - echo "manager=yarn" >> $GITHUB_OUTPUT - - name: Detect package manager id: package-manager run: | @@ -116,6 +82,12 @@ jobs: echo "lockfile=" >> $GITHUB_OUTPUT fi + - name: Install pnpm + if: steps.package-manager.outputs.manager == 'pnpm' + uses: pnpm/action-setup@v2 + with: + version: 10.10.0 + - name: Check for package.json files id: check-package run: | @@ -133,7 +105,7 @@ jobs: case "${{ steps.package-manager.outputs.manager }}" in pnpm) if [ -f "pnpm-lock.yaml" ]; then - /usr/local/bin/pnpm install --frozen-lockfile + pnpm install --frozen-lockfile else echo "No pnpm-lock.yaml found. Skipping dependency installation." fi @@ -283,76 +255,3 @@ jobs: if [ "$BUILD_RAN" = "false" ]; then echo "No build script found. Skipping build step." fi - - e2e-tests: - name: E2E Tests - runs-on: ubuntu-latest - services: - postgres: - image: postgres:15 - env: - POSTGRES_PASSWORD: postgres - POSTGRES_DB: gasguard_test - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - 5432:5432 - redis: - image: redis:7-alpine - ports: - - 6379:6379 - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Install dependencies - run: | - if [ -f "pnpm-lock.yaml" ]; then - pnpm install --frozen-lockfile - else - echo "No pnpm-lock.yaml found." - exit 1 - fi - - - name: Check for E2E test directory - id: check-e2e - run: | - if [ -d "apps/api-service/test/e2e" ]; then - echo "exists=true" >> $GITHUB_OUTPUT - echo "E2E test directory found" - else - echo "exists=false" >> $GITHUB_OUTPUT - echo "E2E test directory not found" - fi - - - name: Run E2E tests - if: steps.check-e2e.outputs.exists == 'true' - run: | - cd apps/api-service - pnpm run test:e2e - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/gasguard_test - REDIS_URL: redis://localhost:6379 - NODE_ENV: test - - - name: Upload test results - if: always() && steps.check-e2e.outputs.exists == 'true' - uses: actions/upload-artifact@v4 - with: - name: e2e-test-results - path: apps/api-service/test-results/ - if-no-files-found: ignore diff --git a/src/analysis/conflicts/conflict-detector.spec.ts b/src/analysis/conflicts/conflict-detector.spec.ts new file mode 100644 index 0000000..10f7c75 --- /dev/null +++ b/src/analysis/conflicts/conflict-detector.spec.ts @@ -0,0 +1,442 @@ +/** + * Conflict Detector Tests + */ + +import { ConflictDetector, ConflictWarner } from './index'; +import { ConflictType, ConflictSeverity, ConflictRule, ResolutionStrategy } from './types'; +import { RuleViolation } from '../pipeline/types'; +import { Suggestion } from '../context/context-aware-suggestions'; + +describe('ConflictDetector', () => { + let detector: ConflictDetector; + + beforeEach(() => { + detector = new ConflictDetector(); + }); + + describe('detectConflicts', () => { + it('should detect no conflicts when violations are unrelated', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'rule-1', + type: 'gas', + severity: 'medium', + message: 'Use uint256 instead of uint8', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'rule-2', + type: 'security', + severity: 'high', + message: 'Add access control', + location: { file: 'contract.sol', line: 20 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'rule-1', message: 'Replace uint8 with uint256' }, + { ruleId: 'rule-2', message: 'Add onlyOwner modifier' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(false); + expect(result.conflicts).toHaveLength(0); + }); + + it('should detect overlapping modifications at same location', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'rule-1', + type: 'gas', + severity: 'medium', + message: 'Remove unused variable', + location: { file: 'contract.sol', line: 10, column: 5 }, + }, + { + ruleId: 'rule-2', + type: 'gas', + severity: 'medium', + message: 'Replace variable with constant', + location: { file: 'contract.sol', line: 10, column: 5 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'rule-1', message: 'Remove the variable x' }, + { ruleId: 'rule-2', message: 'Replace x with constant' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(true); + expect(result.conflicts).toHaveLength(1); + expect(result.conflicts[0].conflictType).toBe(ConflictType.OVERLAPPING_MODIFICATION); + expect(result.conflicts[0].involvedRules).toEqual(['rule-1', 'rule-2']); + }); + + it('should detect opposite actions', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'rule-1', + type: 'gas', + severity: 'medium', + message: 'Add caching', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'rule-2', + type: 'gas', + severity: 'medium', + message: 'Remove caching', + location: { file: 'contract.sol', line: 10 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'rule-1', message: 'Add caching for this variable' }, + { ruleId: 'rule-2', message: 'Remove caching to save gas' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(true); + expect(result.conflicts[0].conflictType).toBe(ConflictType.OPPOSITE_ACTION); + }); + + it('should detect rule-specific conflicts', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'gas-001', + type: 'gas', + severity: 'medium', + message: 'Optimize loop', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'gas-002', + type: 'gas', + severity: 'medium', + message: 'Different optimization', + location: { file: 'contract.sol', line: 20 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'gas-001', message: 'Cache loop variable' }, + { ruleId: 'gas-002', message: 'Unroll loop' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(true); + expect(result.conflicts[0].conflictType).toBe(ConflictType.CONTRADICTORY_OPTIMIZATION); + }); + + it('should detect dependency violations (removal vs usage)', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'unused-001', + type: 'cleanup', + severity: 'low', + message: 'Remove unused variable x', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'usage-001', + type: 'optimization', + severity: 'medium', + message: 'Use variable x for caching', + location: { file: 'contract.sol', line: 15 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'unused-001', message: 'Remove unused variable x' }, + { ruleId: 'usage-001', message: 'Use variable x' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(true); + expect(result.conflicts[0].conflictType).toBe(ConflictType.DEPENDENCY_VIOLATION); + expect(result.conflicts[0].severity).toBe(ConflictSeverity.HIGH); + }); + + it('should respect severity threshold', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'rule-1', + type: 'gas', + severity: 'medium', + message: 'Optimization 1', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'rule-2', + type: 'gas', + severity: 'medium', + message: 'Optimization 2', + location: { file: 'contract.sol', line: 10 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'rule-1', message: 'Remove x' }, + { ruleId: 'rule-2', message: 'Replace x' }, + ]; + + const detectorHighThreshold = new ConflictDetector({ minSeverity: ConflictSeverity.HIGH }); + const result = detectorHighThreshold.detectConflicts(violations, suggestions); + + // Medium severity conflict should be filtered out + expect(result.hasConflicts).toBe(false); + }); + + it('should be disabled when config.enabled is false', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'rule-1', + type: 'gas', + severity: 'medium', + message: 'Optimization 1', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'rule-2', + type: 'gas', + severity: 'medium', + message: 'Optimization 2', + location: { file: 'contract.sol', line: 10 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'rule-1', message: 'Remove x' }, + { ruleId: 'rule-2', message: 'Replace x' }, + ]; + + const disabledDetector = new ConflictDetector({ enabled: false }); + const result = disabledDetector.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(false); + }); + + it('should use custom conflict rules', () => { + const customRule: ConflictRule = { + rulePattern1: 'custom-*', + rulePattern2: 'custom-*', + conflictType: ConflictType.SCOPE_CONFLICT, + severity: ConflictSeverity.MEDIUM, + resolutionStrategy: ResolutionStrategy.PREFER_FIRST, + description: 'Custom rule conflict', + }; + + const detectorWithCustom = new ConflictDetector({ customRules: [customRule] }); + + const violations: RuleViolation[] = [ + { + ruleId: 'custom-001', + type: 'custom', + severity: 'medium', + message: 'Custom rule 1', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'custom-002', + type: 'custom', + severity: 'medium', + message: 'Custom rule 2', + location: { file: 'contract.sol', line: 20 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'custom-001', message: 'Suggestion 1' }, + { ruleId: 'custom-002', message: 'Suggestion 2' }, + ]; + + const result = detectorWithCustom.detectConflicts(violations, suggestions); + + expect(result.hasConflicts).toBe(true); + expect(result.conflicts[0].conflictType).toBe(ConflictType.SCOPE_CONFLICT); + }); + }); + + describe('getResolutionStrategy', () => { + it('should return resolution strategy for known conflicts', () => { + const violations: RuleViolation[] = [ + { + ruleId: 'gas-001', + type: 'gas', + severity: 'medium', + message: 'Optimization', + location: { file: 'contract.sol', line: 10 }, + }, + { + ruleId: 'gas-002', + type: 'gas', + severity: 'medium', + message: 'Other optimization', + location: { file: 'contract.sol', line: 20 }, + }, + ]; + + const suggestions: Suggestion[] = [ + { ruleId: 'gas-001', message: 'Suggestion 1' }, + { ruleId: 'gas-002', message: 'Suggestion 2' }, + ]; + + const result = detector.detectConflicts(violations, suggestions); + const strategy = detector.getResolutionStrategy(result.conflicts[0]); + + expect(strategy).toBe(ResolutionStrategy.REQUIRE_USER_INPUT); + }); + }); +}); + +describe('ConflictWarner', () => { + let warner: ConflictWarner; + + beforeEach(() => { + warner = new ConflictWarner(); + }); + + describe('generateWarnings', () => { + it('should return empty array when no conflicts', () => { + const result = { + hasConflicts: false, + conflicts: [], + conflictCounts: { low: 0, medium: 0, high: 0 }, + }; + + const warnings = warner.generateWarnings(result); + + expect(warnings).toHaveLength(0); + }); + + it('should generate summary warning', () => { + const result = { + hasConflicts: true, + conflicts: [], + conflictCounts: { low: 1, medium: 2, high: 0 }, + }; + + const warnings = warner.generateWarnings(result); + + expect(warnings.length).toBeGreaterThan(0); + expect(warnings[0].message).toContain('3 conflict'); + }); + + it('should mark high severity conflicts as critical', () => { + const result = { + hasConflicts: true, + conflicts: [ + { + conflictType: ConflictType.DEPENDENCY_VIOLATION, + severity: ConflictSeverity.HIGH, + description: 'Critical conflict', + involvedRules: ['rule-1', 'rule-2'], + violations: [], + conflictingSuggestions: [], + resolutionSuggestion: 'Fix it', + }, + ], + conflictCounts: { low: 0, medium: 0, high: 1 }, + }; + + const warnings = warner.generateWarnings(result); + + expect(warnings[1].critical).toBe(true); + expect(warnings[1].severity).toBe(ConflictSeverity.HIGH); + }); + }); + + describe('getStatusMessage', () => { + it('should return success message when no conflicts', () => { + const result = { + hasConflicts: false, + conflicts: [], + conflictCounts: { low: 0, medium: 0, high: 0 }, + }; + + const message = warner.getStatusMessage(result); + + expect(message).toContain('No conflicts'); + }); + + it('should return critical message for high severity conflicts', () => { + const result = { + hasConflicts: true, + conflicts: [], + conflictCounts: { low: 0, medium: 0, high: 2 }, + }; + + const message = warner.getStatusMessage(result); + + expect(message).toContain('critical'); + }); + + it('should return warning message for medium severity conflicts', () => { + const result = { + hasConflicts: true, + conflicts: [], + conflictCounts: { low: 0, medium: 1, high: 0 }, + }; + + const message = warner.getStatusMessage(result); + + expect(message).toContain('âš ī¸'); + }); + }); + + describe('shouldBlockExecution', () => { + it('should block execution when high severity conflicts exist', () => { + const result = { + hasConflicts: true, + conflicts: [], + conflictCounts: { low: 0, medium: 0, high: 1 }, + }; + + expect(warner.shouldBlockExecution(result)).toBe(true); + }); + + it('should not block execution when only low/medium conflicts exist', () => { + const result = { + hasConflicts: true, + conflicts: [], + conflictCounts: { low: 2, medium: 1, high: 0 }, + }; + + expect(warner.shouldBlockExecution(result)).toBe(false); + }); + }); + + describe('generateStructuredWarnings', () => { + it('should generate machine-readable warnings', () => { + const result = { + hasConflicts: true, + conflicts: [ + { + conflictType: ConflictType.OVERLAPPING_MODIFICATION, + severity: ConflictSeverity.MEDIUM, + description: 'Test conflict', + involvedRules: ['rule-1', 'rule-2'], + violations: [], + conflictingSuggestions: [], + location: { file: 'test.sol', line: 10 }, + resolutionSuggestion: 'Resolve manually', + }, + ], + conflictCounts: { low: 0, medium: 1, high: 0 }, + }; + + const structured = warner.generateStructuredWarnings(result); + + expect(structured.summary).toBeDefined(); + expect(structured.conflicts).toHaveLength(1); + expect(structured.conflicts[0].type).toBe('OVERLAPPING_MODIFICATION'); + expect(structured.conflicts[0].location).toBe('test.sol:10'); + }); + }); +}); diff --git a/src/analysis/conflicts/conflict-detector.ts b/src/analysis/conflicts/conflict-detector.ts new file mode 100644 index 0000000..f61ce32 --- /dev/null +++ b/src/analysis/conflicts/conflict-detector.ts @@ -0,0 +1,437 @@ +/** + * Conflict Detector + * + * Detects conflicting rule suggestions and provides resolution strategies + */ + +import { + ConflictInfo, + ConflictType, + ConflictSeverity, + ConflictDetectionResult, + ConflictDetectionConfig, + ConflictRule, + ResolutionStrategy, +} from './types'; +import { RuleViolation, Suggestion } from '../context/context-aware-suggestions'; + +export class ConflictDetector { + private conflictRules: Map = new Map(); + private config: ConflictDetectionConfig; + + constructor(config?: Partial) { + this.config = { + enabled: config?.enabled ?? true, + minSeverity: config?.minSeverity ?? ConflictSeverity.LOW, + customRules: config?.customRules ?? [], + }; + this.initializeDefaultRules(); + this.loadCustomRules(); + } + + /** + * Detect conflicts among a list of violations and suggestions + */ + detectConflicts( + violations: RuleViolation[], + suggestions: Suggestion[] + ): ConflictDetectionResult { + if (!this.config.enabled) { + return { + hasConflicts: false, + conflicts: [], + conflictCounts: { low: 0, medium: 0, high: 0 }, + }; + } + + const conflicts: ConflictInfo[] = []; + + // Check for conflicts between all pairs of violations + for (let i = 0; i < violations.length; i++) { + for (let j = i + 1; j < violations.length; j++) { + const conflict = this.checkPairConflict( + violations[i], + violations[j], + suggestions[i], + suggestions[j] + ); + if (conflict && this.isSeverityAboveThreshold(conflict.severity)) { + conflicts.push(conflict); + } + } + } + + // Check for multi-finding conflicts + conflicts.push(...this.checkMultiFindingConflicts(violations, suggestions)); + + // Count conflicts by severity + const conflictCounts = { + low: conflicts.filter((c) => c.severity === ConflictSeverity.LOW).length, + medium: conflicts.filter((c) => c.severity === ConflictSeverity.MEDIUM).length, + high: conflicts.filter((c) => c.severity === ConflictSeverity.HIGH).length, + }; + + return { + hasConflicts: conflicts.length > 0, + conflicts, + conflictCounts, + }; + } + + /** + * Check if two violations conflict with each other + */ + private checkPairConflict( + violation1: RuleViolation, + violation2: RuleViolation, + suggestion1?: Suggestion, + suggestion2?: Suggestion + ): ConflictInfo | null { + // Check for overlapping modifications at the same location + if (this.isSameLocation(violation1, violation2)) { + if (suggestion1 && suggestion2 && this.areSuggestionsConflicting(suggestion1, suggestion2)) { + return { + conflictType: ConflictType.OVERLAPPING_MODIFICATION, + severity: ConflictSeverity.MEDIUM, + description: `Conflicting suggestions at ${this.formatLocation(violation1)}`, + involvedRules: [violation1.ruleId, violation2.ruleId], + violations: [violation1, violation2], + conflictingSuggestions: [suggestion1, suggestion2], + location: violation1.location, + resolutionSuggestion: this.getResolutionSuggestion(ConflictType.OVERLAPPING_MODIFICATION), + }; + } + } + + // Check for rule-specific conflicts + const ruleConflict = this.checkRuleConflict(violation1.ruleId, violation2.ruleId); + if (ruleConflict) { + return { + conflictType: ruleConflict.conflictType, + severity: ruleConflict.severity, + description: ruleConflict.description || `Rule conflict between ${violation1.ruleId} and ${violation2.ruleId}`, + involvedRules: [violation1.ruleId, violation2.ruleId], + violations: [violation1, violation2], + conflictingSuggestions: suggestion1 && suggestion2 ? [suggestion1, suggestion2] : [], + location: this.getCommonLocation(violation1, violation2), + resolutionSuggestion: this.getResolutionSuggestion(ruleConflict.conflictType), + }; + } + + // Check for opposite actions + if (suggestion1 && suggestion2 && this.areOppositeActions(suggestion1, suggestion2)) { + return { + conflictType: ConflictType.OPPOSITE_ACTION, + severity: ConflictSeverity.HIGH, + description: `Opposite actions suggested: ${violation1.ruleId} vs ${violation2.ruleId}`, + involvedRules: [violation1.ruleId, violation2.ruleId], + violations: [violation1, violation2], + conflictingSuggestions: [suggestion1, suggestion2], + location: this.getCommonLocation(violation1, violation2), + resolutionSuggestion: this.getResolutionSuggestion(ConflictType.OPPOSITE_ACTION), + }; + } + + return null; + } + + /** + * Check for conflicts involving multiple findings + */ + private checkMultiFindingConflicts( + violations: RuleViolation[], + suggestions: Suggestion[] + ): ConflictInfo[] { + const conflicts: ConflictInfo[] = []; + + // Group violations by file + const fileGroups = new Map(); + for (let i = 0; i < violations.length; i++) { + const file = violations[i].location?.file || 'unknown'; + if (!fileGroups.has(file)) { + fileGroups.set(file, { violations: [], suggestions: [] }); + } + fileGroups.get(file)!.violations.push(violations[i]); + fileGroups.get(file)!.suggestions.push(suggestions[i] || { ruleId: violations[i].ruleId, message: violations[i].message }); + } + + // Check for scope conflicts within each file + for (const [file, group] of fileGroups) { + conflicts.push(...this.checkScopeConflicts(group.violations, group.suggestions, file)); + } + + return conflicts; + } + + /** + * Check for scope-related conflicts (e.g., variable removal vs usage) + */ + private checkScopeConflicts( + violations: RuleViolation[], + suggestions: Suggestion[], + file: string + ): ConflictInfo[] { + const conflicts: ConflictInfo[] = []; + + // Find removal suggestions + const removalFindings: { violation: RuleViolation; suggestion: Suggestion }[] = []; + const usageFindings: { violation: RuleViolation; suggestion: Suggestion }[] = []; + + for (let i = 0; i < violations.length; i++) { + const suggestion = suggestions[i]; + if (this.isRemovalSuggestion(violations[i], suggestion)) { + removalFindings.push({ violation: violations[i], suggestion }); + } else if (this.isUsageSuggestion(violations[i], suggestion)) { + usageFindings.push({ violation: violations[i], suggestion }); + } + } + + // Check for conflicts between removal and usage + for (const removal of removalFindings) { + for (const usage of usageFindings) { + if (this.variableUsageConflicts(removal.violation, usage.violation)) { + conflicts.push({ + conflictType: ConflictType.DEPENDENCY_VIOLATION, + severity: ConflictSeverity.HIGH, + description: `Variable removal conflicts with usage in ${file}`, + involvedRules: [removal.violation.ruleId, usage.violation.ruleId], + violations: [removal.violation, usage.violation], + conflictingSuggestions: [removal.suggestion, usage.suggestion], + location: removal.violation.location, + resolutionSuggestion: this.getResolutionSuggestion(ConflictType.DEPENDENCY_VIOLATION), + }); + } + } + } + + return conflicts; + } + + /** + * Check if two violations are at the same location + */ + private isSameLocation(v1: RuleViolation, v2: RuleViolation): boolean { + return ( + v1.location?.file === v2.location?.file && + v1.location?.line === v2.location?.line && + v1.location?.column === v2.location?.column + ); + } + + /** + * Check if two suggestions conflict with each other + */ + private areSuggestionsConflicting(s1: Suggestion, s2: Suggestion): boolean { + // Different suggestions that both modify code + if (s1.message !== s2.message) { + const modifies1 = this.isModificationSuggestion(s1); + const modifies2 = this.isModificationSuggestion(s2); + return modifies1 && modifies2; + } + return false; + } + + /** + * Check if suggestions suggest opposite actions + */ + private areOppositeActions(s1: Suggestion, s2: Suggestion): boolean { + const opposites = [ + ['add', 'remove'], + ['include', 'exclude'], + ['enable', 'disable'], + ['use', 'avoid'], + ['cache', 'bypass'], + ]; + + const msg1 = s1.message.toLowerCase(); + const msg2 = s2.message.toLowerCase(); + + for (const [op1, op2] of opposites) { + if (msg1.includes(op1) && msg2.includes(op2)) { + return true; + } + if (msg1.includes(op2) && msg2.includes(op1)) { + return true; + } + } + + return false; + } + + /** + * Check if a suggestion is a modification suggestion + */ + private isModificationSuggestion(s: Suggestion): boolean { + const keywords = ['remove', 'replace', 'change', 'modify', 'delete', 'add']; + return keywords.some((kw) => s.message.toLowerCase().includes(kw)); + } + + /** + * Check if a violation/suggestion is about removal + */ + private isRemovalSuggestion(v: RuleViolation, s?: Suggestion): boolean { + const msg = (s?.message || v.message).toLowerCase(); + return msg.includes('remove') || msg.includes('delete') || msg.includes('unused'); + } + + /** + * Check if a violation/suggestion is about usage + */ + private isUsageSuggestion(v: RuleViolation, s?: Suggestion): boolean { + const msg = (s?.message || v.message).toLowerCase(); + return msg.includes('use') || msg.includes('access') || msg.includes('reference'); + } + + /** + * Check if variable removal conflicts with usage + */ + private variableUsageConflicts(removal: RuleViolation, usage: RuleViolation): boolean { + if (removal.location?.file !== usage.location?.file) { + return false; + } + const lineDiff = Math.abs((removal.location?.line || 0) - (usage.location?.line || 0)); + return lineDiff < 10; // Within 10 lines + } + + /** + * Check if two rules have a defined conflict + */ + private checkRuleConflict(ruleId1: string, ruleId2: string): ConflictRule | null { + for (const [pattern, rules] of this.conflictRules) { + if (this.matchesPattern(ruleId1, pattern)) { + for (const rule of rules) { + if (this.matchesPattern(ruleId2, rule.rulePattern2)) { + return rule; + } + } + } + } + return null; + } + + /** + * Check if a rule ID matches a pattern (supports wildcards) + */ + private matchesPattern(ruleId: string, pattern: string): boolean { + if (pattern === '*') return true; + if (pattern.includes('*')) { + const regex = new RegExp(pattern.replace(/\*/g, '.*')); + return regex.test(ruleId); + } + return ruleId === pattern; + } + + /** + * Get common location between two violations + */ + private getCommonLocation(v1: RuleViolation, v2: RuleViolation): { file?: string; line?: number } | undefined { + if (v1.location?.file === v2.location?.file) { + return { + file: v1.location?.file, + line: v1.location?.line, + }; + } + return undefined; + } + + /** + * Format location for display + */ + private formatLocation(v: RuleViolation): string { + if (!v.location) return 'unknown location'; + return `${v.location.file}:${v.location.line}${v.location.column ? `:${v.location.column}` : ''}`; + } + + /** + * Check if severity is above the configured threshold + */ + private isSeverityAboveThreshold(severity: ConflictSeverity): boolean { + const order = [ConflictSeverity.LOW, ConflictSeverity.MEDIUM, ConflictSeverity.HIGH]; + const thresholdIndex = order.indexOf(this.config.minSeverity); + const severityIndex = order.indexOf(severity); + return severityIndex >= thresholdIndex; + } + + /** + * Get resolution suggestion for a conflict type + */ + private getResolutionSuggestion(conflictType: ConflictType): string { + switch (conflictType) { + case ConflictType.OVERLAPPING_MODIFICATION: + return 'Consider applying only one of the conflicting optimizations or merge manually.'; + case ConflictType.CONTRADICTORY_OPTIMIZATION: + return 'These optimizations contradict each other. Choose the one with higher impact.'; + case ConflictType.DEPENDENCY_VIOLATION: + return 'One optimization depends on code that another wants to remove. Review dependencies.'; + case ConflictType.SCOPE_CONFLICT: + return 'Scope-related conflict. Check if optimizations affect the same variable/function scope.'; + case ConflictType.OPPOSITE_ACTION: + return 'Rules suggest opposite actions. Review which action is appropriate for your use case.'; + } + } + + /** + * Initialize default conflict rules + */ + private initializeDefaultRules(): void { + // Gas optimization conflicts + this.addConflictRule({ + rulePattern1: 'gas-*', + rulePattern2: 'gas-*', + conflictType: ConflictType.CONTRADICTORY_OPTIMIZATION, + severity: ConflictSeverity.MEDIUM, + resolutionStrategy: ResolutionStrategy.REQUIRE_USER_INPUT, + description: 'Multiple gas optimizations may conflict', + }); + + // Security vs optimization conflicts + this.addConflictRule({ + rulePattern1: 'security-*', + rulePattern2: 'gas-*', + conflictType: ConflictType.CONTRADICTORY_OPTIMIZATION, + severity: ConflictSeverity.HIGH, + resolutionStrategy: ResolutionStrategy.PREFER_FIRST, + description: 'Security rules take precedence over gas optimizations', + }); + + // Unused variable conflicts + this.addConflictRule({ + rulePattern1: '*unused*', + rulePattern2: '*usage*', + conflictType: ConflictType.DEPENDENCY_VIOLATION, + severity: ConflictSeverity.HIGH, + resolutionStrategy: ResolutionStrategy.REQUIRE_USER_INPUT, + description: 'Variable removal may conflict with usage', + }); + } + + /** + * Load custom conflict rules from config + */ + private loadCustomRules(): void { + if (this.config.customRules) { + for (const rule of this.config.customRules) { + this.addConflictRule(rule); + } + } + } + + /** + * Add a conflict rule + */ + private addConflictRule(rule: ConflictRule): void { + const pattern = rule.rulePattern1; + if (!this.conflictRules.has(pattern)) { + this.conflictRules.set(pattern, []); + } + this.conflictRules.get(pattern)!.push(rule); + } + + /** + * Get resolution strategy for a conflict + */ + public getResolutionStrategy(conflict: ConflictInfo): ResolutionStrategy { + const ruleConflict = this.checkRuleConflict(conflict.involvedRules[0], conflict.involvedRules[1]); + return ruleConflict?.resolutionStrategy || ResolutionStrategy.REQUIRE_USER_INPUT; + } +} diff --git a/src/analysis/conflicts/conflict-warned.ts b/src/analysis/conflicts/conflict-warned.ts new file mode 100644 index 0000000..a48f61b --- /dev/null +++ b/src/analysis/conflicts/conflict-warned.ts @@ -0,0 +1,242 @@ +/** + * Conflict Warning System + * + * Provides user-facing warnings for detected conflicts + */ + +import { + ConflictInfo, + ConflictDetectionResult, + ConflictSeverity, +} from './types'; + +export interface WarningOutput { + /** Formatted warning message */ + message: string; + /** Severity level for display */ + severity: ConflictSeverity; + /** Whether this is a critical warning that requires attention */ + critical: boolean; + /** Suggested actions */ + actions: string[]; +} + +export class ConflictWarner { + /** + * Generate user-friendly warnings from conflict detection results + */ + generateWarnings(result: ConflictDetectionResult): WarningOutput[] { + if (!result.hasConflicts) { + return []; + } + + const warnings: WarningOutput[] = []; + + // Group conflicts by severity + const highSeverityConflicts = result.conflicts.filter( + (c) => c.severity === ConflictSeverity.HIGH + ); + const mediumSeverityConflicts = result.conflicts.filter( + (c) => c.severity === ConflictSeverity.MEDIUM + ); + const lowSeverityConflicts = result.conflicts.filter( + (c) => c.severity === ConflictSeverity.LOW + ); + + // Generate summary warning + warnings.push(this.generateSummaryWarning(result)); + + // Generate individual conflict warnings + for (const conflict of highSeverityConflicts) { + warnings.push(this.generateConflictWarning(conflict, true)); + } + + for (const conflict of mediumSeverityConflicts) { + warnings.push(this.generateConflictWarning(conflict, false)); + } + + // Low severity conflicts only shown in verbose mode + for (const conflict of lowSeverityConflicts) { + warnings.push(this.generateConflictWarning(conflict, false)); + } + + return warnings; + } + + /** + * Generate a summary warning for all conflicts + */ + private generateSummaryWarning(result: ConflictDetectionResult): WarningOutput { + const { conflicts, conflictCounts } = result; + const total = conflicts.length; + + let message = `âš ī¸ Detected ${total} conflict${total > 1 ? 's' : ''} between rule suggestions.`; + + if (conflictCounts.high > 0) { + message += ` ${conflictCounts.high} critical,`; + } + if (conflictCounts.medium > 0) { + message += ` ${conflictCounts.medium} medium,`; + } + if (conflictCounts.low > 0) { + message += ` ${conflictCounts.low} low`; + } + + message = message.replace(/,\s*$/, '.'); + message += ' Review conflicts before applying fixes.'; + + const actions: string[] = []; + if (conflictCounts.high > 0) { + actions.push('Review critical conflicts immediately'); + } + if (conflictCounts.medium > 0) { + actions.push('Choose between conflicting optimizations'); + } + actions.push('Consider applying fixes selectively'); + + return { + message, + severity: conflictCounts.high > 0 ? ConflictSeverity.HIGH : ConflictSeverity.MEDIUM, + critical: conflictCounts.high > 0, + actions, + }; + } + + /** + * Generate a warning for a specific conflict + */ + private generateConflictWarning(conflict: ConflictInfo, critical: boolean): WarningOutput { + const location = conflict.location + ? `${conflict.location.file}:${conflict.location.line}` + : 'multiple locations'; + + const rules = conflict.involvedRules.join(' vs '); + + let message = `${critical ? '🚨' : 'âš ī¸'} Conflict: ${conflict.description}`; + message += `\n Rules: ${rules}`; + if (location !== 'multiple locations') { + message += `\n Location: ${location}`; + } + if (conflict.resolutionSuggestion) { + message += `\n Suggestion: ${conflict.resolutionSuggestion}`; + } + + const actions: string[] = []; + switch (conflict.conflictType) { + case 'OVERLAPPING_MODIFICATION': + actions.push('Apply only one suggestion'); + actions.push('Or merge manually if compatible'); + break; + case 'CONTRADICTORY_OPTIMIZATION': + actions.push('Choose the higher-impact optimization'); + actions.push('Or disable one of the conflicting rules'); + break; + case 'DEPENDENCY_VIOLATION': + actions.push('Review code dependencies'); + actions.push('Keep the required code'); + break; + case 'SCOPE_CONFLICT': + actions.push('Check variable/function scopes'); + actions.push('Ensure changes are isolated'); + break; + case 'OPPOSITE_ACTION': + actions.push('Determine which action is appropriate'); + actions.push('Review the context of each suggestion'); + break; + } + + return { + message, + severity: conflict.severity, + critical, + actions, + }; + } + + /** + * Print warnings to console in a formatted way + */ + printWarnings(result: ConflictDetectionResult): void { + const warnings = this.generateWarnings(result); + + if (warnings.length === 0) { + return; + } + + console.log('\n' + '='.repeat(80)); + console.log('CONFLICT DETECTION REPORT'); + console.log('='.repeat(80) + '\n'); + + for (const warning of warnings) { + const icon = warning.critical ? '🚨' : 'âš ī¸'; + const severity = warning.severity.toUpperCase(); + + console.log(`${icon} [${severity}] ${warning.message}\n`); + + if (warning.actions.length > 0) { + console.log(' Suggested actions:'); + for (const action of warning.actions) { + console.log(` â€ĸ ${action}`); + } + console.log(); + } + } + + console.log('='.repeat(80) + '\n'); + } + + /** + * Generate a machine-readable warning format (e.g., for JSON output) + */ + generateStructuredWarnings(result: ConflictDetectionResult): { + summary: string; + conflicts: Array<{ + type: string; + severity: string; + description: string; + rules: string[]; + location?: string; + resolution: string; + }>; + } { + return { + summary: `Detected ${result.conflicts.length} conflicts`, + conflicts: result.conflicts.map((conflict) => ({ + type: conflict.conflictType, + severity: conflict.severity, + description: conflict.description, + rules: conflict.involvedRules, + location: conflict.location + ? `${conflict.location.file}:${conflict.location.line}` + : undefined, + resolution: conflict.resolutionSuggestion || 'No resolution suggested', + })), + }; + } + + /** + * Check if conflicts should block execution + */ + shouldBlockExecution(result: ConflictDetectionResult): boolean { + return result.conflictCounts.high > 0; + } + + /** + * Get a quick status message + */ + getStatusMessage(result: ConflictDetectionResult): string { + if (!result.hasConflicts) { + return '✅ No conflicts detected'; + } + + if (result.conflictCounts.high > 0) { + return `🚨 ${result.conflictCounts.high} critical conflict(s) detected`; + } + + if (result.conflictCounts.medium > 0) { + return `âš ī¸ ${result.conflictCounts.medium} conflict(s) detected`; + } + + return `â„šī¸ ${result.conflictCounts.low} minor conflict(s) detected`; + } +} diff --git a/src/analysis/conflicts/index.ts b/src/analysis/conflicts/index.ts new file mode 100644 index 0000000..20fcd94 --- /dev/null +++ b/src/analysis/conflicts/index.ts @@ -0,0 +1,9 @@ +/** + * Conflict Detection Module + * + * Exports the conflict detection system for identifying conflicting rule suggestions + */ + +export * from './types'; +export { ConflictDetector } from './conflict-detector'; +export { ConflictWarner } from './conflict-warned'; diff --git a/src/analysis/conflicts/types.ts b/src/analysis/conflicts/types.ts new file mode 100644 index 0000000..2798129 --- /dev/null +++ b/src/analysis/conflicts/types.ts @@ -0,0 +1,109 @@ +/** + * Conflict Detection Types + * + * Defines the data structures for detecting and reporting conflicting rule suggestions + */ + +import { RuleViolation, Suggestion } from '../context/context-aware-suggestions'; + +/** Types of conflicts that can occur between rule suggestions */ +export enum ConflictType { + /** Two rules suggest different modifications to the same code location */ + OVERLAPPING_MODIFICATION = 'OVERLAPPING_MODIFICATION', + /** Rules suggest contradictory optimizations (e.g., cache vs remove) */ + CONTRADICTORY_OPTIMIZATION = 'CONTRADICTORY_OPTIMIZATION', + /** One rule depends on code another rule wants to remove */ + DEPENDENCY_VIOLATION = 'DEPENDENCY_VIOLATION', + /** Rules affect the same variable/function scope in conflicting ways */ + SCOPE_CONFLICT = 'SCOPE_CONFLICT', + /** Rules suggest opposite actions (e.g., add vs remove) */ + OPPOSITE_ACTION = 'OPPOSITE_ACTION', +} + +/** Severity level of the conflict */ +export enum ConflictSeverity { + /** Can be resolved automatically or safely ignored */ + LOW = 'LOW', + /** Requires user choice between alternatives */ + MEDIUM = 'MEDIUM', + /** Should not be merged - critical conflict */ + HIGH = 'HIGH', +} + +/** Information about a detected conflict */ +export interface ConflictInfo { + /** Type of conflict detected */ + conflictType: ConflictType; + /** Severity of the conflict */ + severity: ConflictSeverity; + /** Human-readable description of the conflict */ + description: string; + /** Rule IDs involved in the conflict */ + involvedRules: string[]; + /** Violations involved in the conflict */ + violations: RuleViolation[]; + /** Suggestions that conflict with each other */ + conflictingSuggestions: Suggestion[]; + /** Location of the conflict (if applicable) */ + location?: { + file?: string; + line?: number; + column?: number; + }; + /** Suggested resolution for the conflict */ + resolutionSuggestion?: string; +} + +/** Result of conflict detection */ +export interface ConflictDetectionResult { + /** Whether any conflicts were detected */ + hasConflicts: boolean; + /** List of all detected conflicts */ + conflicts: ConflictInfo[]; + /** Total number of conflicts by severity */ + conflictCounts: { + low: number; + medium: number; + high: number; + }; +} + +/** Configuration for conflict detection */ +export interface ConflictDetectionConfig { + /** Whether to enable conflict detection */ + enabled: boolean; + /** Minimum severity level to report */ + minSeverity: ConflictSeverity; + /** Custom conflict rules */ + customRules?: ConflictRule[]; +} + +/** Rule defining when two specific rules conflict */ +export interface ConflictRule { + /** Pattern for the first rule ID (can use wildcards) */ + rulePattern1: string; + /** Pattern for the second rule ID (can use wildcards) */ + rulePattern2: string; + /** Type of conflict that occurs */ + conflictType: ConflictType; + /** Severity of the conflict */ + severity: ConflictSeverity; + /** Custom description (optional) */ + description?: string; + /** Resolution strategy */ + resolutionStrategy: ResolutionStrategy; +} + +/** Strategy for resolving conflicts */ +export enum ResolutionStrategy { + /** Prefer the first rule's suggestion */ + PREFER_FIRST = 'PREFER_FIRST', + /** Prefer the second rule's suggestion */ + PREFER_SECOND = 'PREFER_SECOND', + /** Try to merge if compatible */ + MERGE_IF_COMPATIBLE = 'MERGE_IF_COMPATIBLE', + /** Require user input to resolve */ + REQUIRE_USER_INPUT = 'REQUIRE_USER_INPUT', + /** Apply both if they don't conflict */ + APPLY_BOTH = 'APPLY_BOTH', +}