UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

746 lines (610 loc) 22.5 kB
# Dart Rule Execution Flow > Tài liệu mô tả chi tiết luồng hoạt động của một rule khi phân tích code Dart trong SunLint. > Lấy ví dụ: **Rule C002 - No Duplicate Code** ## 1. Tổng quan Architecture ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ SUNLINT ARCHITECTURE │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌──────────────────┐ ┌─────────────────────────────┐ │ │ │ CLI │───▶│ Heuristic │───▶│ DartAnalyzer (JS) │ │ │ │ (cli.js) │ │ Engine │ │ (dart-analyzer.js) │ │ │ └─────────────┘ └──────────────────┘ └──────────────┬──────────────┘ │ │ │ │ │ JSON-RPC over STDIO │ │ │ │ │ ┌──────────────────────────────────────▼──────────────┐ │ │ │ DART BINARY (sunlint-dart-macos) │ │ │ │ ┌─────────────────┐ ┌──────────────────────────┐ │ │ │ │ │ JsonRpcServer │ │ AnalyzerService │ │ │ │ │ └────────┬────────┘ └────────────┬─────────────┘ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ ┌─────────────────────────────────────────────┐ │ │ │ │ │ lib/rules/C002_no_duplicate_code.dart │ │ │ │ │ └─────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` --- ## 2. Chi tiết từng bước ### Step 1: CLI Command ```bash node cli.js --rule=C002 \ --input="examples/rule-test-fixtures/dart-rules/C002_no_duplicate_code/violations" \ --engine=heuristic \ --languages=dart \ --include="**/*.dart" ``` **File:** `cli.js` **Chức năng:** - Parse command line arguments - Load configuration - Initialize engines - Dispatch to appropriate engine --- ### Step 2: Heuristic Engine **File:** `engines/heuristic-engine.js` #### 2.1. Detect Dart Support ```javascript // Line 27-51 function detectDartSupport(ruleId) { const rulesBasePath = path.join(__dirname, '../rules'); const categories = ['common', 'security', 'typescript']; for (const category of categories) { const categoryPath = path.join(rulesBasePath, category); // Scan for: rules/common/C002_no_duplicate_code/dart/ for (const folder of ruleFolders) { if (folder.startsWith(ruleId + '_')) { const dartPath = path.join(categoryPath, folder, 'dart'); if (fs.existsSync(dartPath)) { return true; // ✅ C002 supports Dart } } } } return false; } ``` #### 2.2. Check DartAnalyzer Availability ```javascript // Line 922-927 const isDartRule = detectDartSupport(rule.id); // true for C002 const hasDartFiles = filesByLanguage['dart'].length > 0; const dartAnalyzer = this.semanticEngineManager?.getAnalyzer('dart'); const useDartAnalyzer = isDartRule && hasDartFiles && dartAnalyzer?.isReady(); ``` #### 2.3. Call DartAnalyzer ```javascript // Line 948-972 if (useDartAnalyzer) { const dartFiles = filesByLanguage['dart']; const rules = [{ id: rule.id, config: rule.config || {} }]; for (const filePath of dartFiles) { const violations = await dartAnalyzer.analyzeFile(filePath, rules, options); ruleViolations.push(...violations); } } ``` --- ### Step 3: Dart Analyzer Client (JavaScript) **File:** `core/adapters/dart-analyzer.js` #### 3.1. Resolve Binary ```javascript // Line 288-337 async resolveBinary() { const platform = process.platform; const binaryName = platform === 'darwin' ? 'sunlint-dart-macos' : `sunlint-dart-${platform}`; // Priority 1: Bundled binary const bundledPath = path.join(__dirname, '../../dart_analyzer/bin', binaryName); if (fs.existsSync(bundledPath)) { return bundledPath; } // ... fallback options } ``` #### 3.2. Start Subprocess ```javascript // Line 35-81 async start(binaryPath) { this.process = spawn(binaryPath, [], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } }); // Handle stdout (JSON-RPC responses) this.process.stdout.on('data', (data) => { this.handleData(data.toString()); }); } ``` #### 3.3. Send JSON-RPC Request ```javascript // Line 368-405 async analyzeFile(filePath, rules, options = {}) { if (this.client && this.client.isConnected()) { // Send JSON-RPC request const result = await this.client.sendRequest('analyze', { filePath, rules: rules.map(r => ({ id: r.id || r.ruleId, config: r.config || {} })) }); violations = result.violations || []; } } ``` **JSON-RPC Request Format:** ```json { "jsonrpc": "2.0", "id": 1, "method": "analyze", "params": { "filePath": "/path/to/file.dart", "rules": [{ "id": "C002", "config": {} }] } } ``` --- ### Step 4: Dart Binary - JSON-RPC Server **File:** `dart_analyzer/lib/json_rpc_server.dart` #### 4.1. Receive Request ```dart // Line 48-64 Future<void> _handleLine(String line) async { final request = jsonDecode(line) as Map<String, dynamic>; final response = await _processRequest(request); _sendResponse(response); } ``` #### 4.2. Route to Handler ```dart // Line 106-122 Future<dynamic> _invokeMethod(String method, Map<String, dynamic> params) async { switch (method) { case 'initialize': return await _handleInitialize(params); case 'analyze': return await _handleAnalyze(params); // ← C002 goes here case 'getSymbolTable': return await _handleGetSymbolTable(params); // ... } } ``` #### 4.3. Handle Analyze Request ```dart // Line 156-174 Future<Map<String, dynamic>> _handleAnalyze(Map<String, dynamic> params) async { final filePath = params['filePath'] as String?; final rulesData = params['rules'] as List<dynamic>?; final violations = await _analyzerService.analyzeFile( filePath: filePath!, rulesData: rulesData, ); return { 'violations': violations.map((v) => v.toJson()).toList(), 'fileAnalyzed': filePath, }; } ``` --- ### Step 5: Analyzer Service **File:** `dart_analyzer/lib/analyzer_service.dart` #### 5.1. Register Analyzers ```dart // Line 36-46 void _registerAnalyzers() { // Common rules (C-series) _analyzers['C002'] = C002NoDuplicateCodeAnalyzer(); _analyzers['C003'] = C003NoVagueAbbreviationsAnalyzer(); // Security rules (S-series) _analyzers['S003'] = S003OpenRedirectProtectionAnalyzer(); _analyzers['S004'] = S004SensitiveDataLoggingAnalyzer(); } ``` #### 5.2. Analyze File ```dart // Line 204-257 Future<List<Violation>> analyzeFile({ required String filePath, List<dynamic>? rulesData, }) async { // Get resolved AST from Dart Analyzer package final context = _contextCollection!.contextFor(absolutePath); final result = await context.currentSession.getResolvedUnit(absolutePath); final unit = result.unit; final violations = <Violation>[]; // Run each applicable analyzer for (final rule in rules) { final analyzer = _analyzers[rule.id]; // Get C002 analyzer if (analyzer == null) continue; final ruleViolations = analyzer.analyze( unit: unit, filePath: absolutePath, rule: rule, lineInfo: result.lineInfo, ); violations.addAll(ruleViolations); } return violations; } ``` --- ### Step 6: C002 Rule Analyzer **File:** `dart_analyzer/lib/rules/C002_no_duplicate_code.dart` #### 6.1. Analyzer Class ```dart class C002NoDuplicateCodeAnalyzer extends BaseAnalyzer { @override String get ruleId => 'C002'; static const int minDuplicateLines = 10; static const double similarityThreshold = 0.85; final Map<String, List<_CodeBlock>> _codeBlocks = {}; } ``` #### 6.2. Analyze Method ```dart // Line 33-51 @override List<Violation> analyze({ required CompilationUnit unit, required String filePath, required Rule rule, required LineInfo lineInfo, }) { final violations = <Violation>[]; // Create AST visitor final visitor = _DuplicateCodeVisitor( filePath: filePath, lineInfo: lineInfo, violations: violations, analyzer: this, codeBlocks: _codeBlocks, ); // Traverse AST unit.accept(visitor); // Detect duplicates _detectDuplicates(violations); return violations; } ``` #### 6.3. AST Visitor ```dart class _DuplicateCodeVisitor extends RecursiveAstVisitor<void> { @override void visitFunctionDeclaration(FunctionDeclaration node) { final body = node.functionExpression.body; if (body is BlockFunctionBody) { _processBlock(body.block, 'function', node.name.lexeme); } super.visitFunctionDeclaration(node); } @override void visitMethodDeclaration(MethodDeclaration node) { final body = node.body; if (body is BlockFunctionBody) { _processBlock(body.block, 'method', node.name.lexeme); } super.visitMethodDeclaration(node); } } ``` #### 6.4. Process Code Block ```dart void _processBlock(Block block, String type, String name) { final startLine = analyzer.getLine(lineInfo, block.offset); final endLine = analyzer.getLine(lineInfo, block.end); final lineCount = endLine - startLine + 1; // Skip if less than 10 lines if (lineCount < C002NoDuplicateCodeAnalyzer.minDuplicateLines) return; // Normalize code for comparison final normalizedLines = _normalizeBlock(block); final hash = _hashLines(normalizedLines); // Store for comparison codeBlocks.putIfAbsent(filePath, () => []).add(_CodeBlock( filePath: filePath, startLine: startLine, endLine: endLine, hash: hash, normalizedLines: normalizedLines, type: '$type:$name', )); } ``` #### 6.5. Detect Duplicates ```dart void _detectDuplicates(List<Violation> violations) { final allBlocks = _codeBlocks.values.expand((b) => b).toList(); for (var i = 0; i < allBlocks.length; i++) { for (var j = i + 1; j < allBlocks.length; j++) { final block1 = allBlocks[i]; final block2 = allBlocks[j]; // Check for exact match via hash if (block1.hash == block2.hash) { violations.add(createViolation( filePath: block2.filePath, line: block2.startLine, column: 1, message: 'Duplicate code block found (${block2.lineCount} lines)...', severity: 'warning', )); } // Check for similar code (LCS algorithm) else { final similarity = _calculateSimilarity(block1, block2); if (similarity >= similarityThreshold) { violations.add(createViolation(...)); } } } } } ``` --- ### Step 7: Response Flow ``` C002 Analyzer ↓ List<Violation> AnalyzerService ↓ violations.map((v) => v.toJson()) JsonRpcServer ↓ JSON-RPC Response DartAnalyzerClient (JS) ↓ result.violations HeuristicEngine ↓ ruleViolations CLI Output ``` **JSON-RPC Response Format:** ```json { "jsonrpc": "2.0", "id": 1, "result": { "violations": [ { "ruleId": "C002", "filePath": "/path/to/file.dart", "line": 31, "column": 1, "message": "Duplicate code block found (19 lines) - same as line 10", "severity": "warning", "analysisMethod": "ast", "metadata": { "duplicateOf": "/path/to/file.dart:10", "lineCount": 19, "similarity": 1.0 } } ], "fileAnalyzed": "/path/to/file.dart" } } ``` --- ## 3. File Structure ### Rule Definition (TypeScript side) ``` rules/common/C002_no_duplicate_code/ ├── config.json # Rule configuration ├── index.js # Router (multi-language) ├── typescript/ │ └── analyzer.js # TypeScript analyzer ├── dart/ │ └── analyzer.js # JS wrapper (delegates to binary) └── test-cases/ # TypeScript test cases ``` ### Rule Implementation (Dart side) ``` dart_analyzer/ ├── bin/ │ ├── sunlint_dart_analyzer.dart # Entry point │ └── sunlint-dart-macos # Compiled binary ├── lib/ │ ├── analyzer_service.dart # Main service │ ├── json_rpc_server.dart # JSON-RPC handler │ ├── models/ │ │ ├── rule.dart │ │ └── violation.dart │ └── rules/ │ ├── base_analyzer.dart # Base class │ ├── C002_no_duplicate_code.dart │ ├── C003_no_vague_abbreviations.dart │ ├── S003_open_redirect_protection.dart │ └── S004_sensitive_data_logging.dart └── pubspec.yaml ``` ### Test Fixtures ``` examples/rule-test-fixtures/dart-rules/ ├── C002_no_duplicate_code/ │ ├── clean/ # ✅ No violations expected │ │ ├── data_validator.dart │ │ └── payment_service.dart │ └── violations/ # ❌ Violations expected │ ├── data_processor.dart │ └── user_service.dart ├── C003_no_vague_abbreviations/ │ ├── clean/ │ └── violations/ ├── S003_open_redirect_protection/ │ ├── clean/ │ └── violations/ └── S004_sensitive_data_logging/ ├── clean/ └── violations/ ``` --- ## 4. Commands ### Run Single Rule on Violations ```bash # From sunlint directory cd /path/to/sunlint # Run C002 on violations folder node cli.js --rule=C002 \ --input="examples/rule-test-fixtures/dart-rules/C002_no_duplicate_code/violations" \ --languages=dart \ --include="**/*.dart" ``` ### Run Single Rule on Clean Code ```bash node cli.js --rule=C002 \ --input="examples/rule-test-fixtures/dart-rules/C002_no_duplicate_code/clean" \ --languages=dart \ --include="**/*.dart" ``` ### Run All Dart Rules ```bash node cli.js --rule=C002,C003,S003,S004 \ --input="examples/rule-test-fixtures/dart-rules" \ --languages=dart \ --include="**/*.dart" ``` ### Verbose Mode ```bash node cli.js --rule=C002 \ --input="examples/rule-test-fixtures/dart-rules/C002_no_duplicate_code/violations" \ --languages=dart \ --include="**/*.dart" \ --verbose ``` --- ## 5. Adding a New Dart Rule ### Step 1: Create TypeScript Rule Structure ```bash mkdir -p rules/common/CXXX_rule_name/{typescript,dart} ``` ### Step 2: Create config.json ```json { "id": "CXXX", "name": "Rule Name", "description": "Rule description", "category": "common", "severity": "warning", "languages": ["typescript", "javascript", "dart"] } ``` ### Step 3: Create index.js Router ```javascript // rules/common/CXXX_rule_name/index.js const path = require('path'); class CXXXRouter { getAnalyzer(language) { const normalizedLang = this.normalizeLanguage(language); const analyzerPath = path.join(__dirname, normalizedLang, 'analyzer.js'); return require(analyzerPath); } // ... } module.exports = new CXXXRouter(); ``` ### Step 4: Create Dart Analyzer ```dart // dart_analyzer/lib/rules/CXXX_rule_name.dart class CXXXRuleAnalyzer extends BaseAnalyzer { @override String get ruleId => 'CXXX'; @override List<Violation> analyze({ required CompilationUnit unit, required String filePath, required Rule rule, required LineInfo lineInfo, }) { final violations = <Violation>[]; // Implementation return violations; } } ``` ### Step 5: Register in AnalyzerService ```dart // dart_analyzer/lib/analyzer_service.dart void _registerAnalyzers() { // ... _analyzers['CXXX'] = CXXXRuleAnalyzer(); } ``` ### Step 6: Create Test Fixtures ``` examples/rule-test-fixtures/dart-rules/CXXX_rule_name/ ├── clean/ │ └── good_example.dart └── violations/ └── bad_example.dart ``` ### Step 7: Rebuild Dart Binary ```bash cd dart_analyzer dart pub get dart compile exe bin/sunlint_dart_analyzer.dart -o bin/sunlint-dart-macos ``` --- ## 6. Troubleshooting ### No files found ```bash # Ensure --languages=dart and --include="**/*.dart" are set node cli.js --rule=C002 --input=path --languages=dart --include="**/*.dart" ``` ### DartAnalyzer not initialized ```bash # Check if binary exists ls -la dart_analyzer/bin/sunlint-dart-macos # Rebuild if needed cd dart_analyzer && dart compile exe bin/sunlint_dart_analyzer.dart -o bin/sunlint-dart-macos ``` ### Rule not supported ```bash # Check if dart/ folder exists in rule directory ls -la rules/common/C002_no_duplicate_code/dart/ # Check if rule is registered in analyzer_service.dart grep "C002" dart_analyzer/lib/analyzer_service.dart ``` --- ## 7. Summary Diagram ``` ┌────────────────────────────────────────────────────────────────────────────┐ │ C002 EXECUTION FLOW │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. CLI Command │ │ └── node cli.js --rule=C002 --input=... --languages=dart │ │ │ │ │ 2. Heuristic Engine ▼ │ │ ├── detectDartSupport('C002') → true │ │ ├── Get DartAnalyzer from SemanticEngineManager │ │ └── Call dartAnalyzer.analyzeFile(filePath, rules) │ │ │ │ │ 3. DartAnalyzer (JS) ▼ │ │ ├── Connect to sunlint-dart-macos binary │ │ └── Send JSON-RPC: { method: 'analyze', params: {...} } │ │ │ │ │ 4. JSON-RPC Server ▼ (Dart Binary) │ │ ├── Receive request on stdin │ │ └── Route to _handleAnalyze() │ │ │ │ │ 5. AnalyzerService ▼ │ │ ├── _analyzers['C002'] → C002NoDuplicateCodeAnalyzer │ │ ├── Get ResolvedUnitResult from Dart Analyzer package │ │ └── Call analyzer.analyze(unit, filePath, rule, lineInfo) │ │ │ │ │ 6. C002 Analyzer ▼ │ │ ├── Create _DuplicateCodeVisitor │ │ ├── Traverse AST: unit.accept(visitor) │ │ ├── Collect code blocks ≥ 10 lines │ │ ├── Normalize & hash blocks │ │ ├── Compare for duplicates (exact match or similarity) │ │ └── Return List<Violation> │ │ │ │ │ 7. Response ▼ │ │ └── Violations → JSON → stdout → JS client → CLI output │ │ │ └────────────────────────────────────────────────────────────────────────────┘ ```