@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
Markdown
# 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 │
│ │
└────────────────────────────────────────────────────────────────────────────┘
```