@sun-asterisk/sunlint 1.3.43 → 1.3.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/source/line_info.dart';
|
|
3
|
+
|
|
4
|
+
import '../models/rule.dart';
|
|
5
|
+
import '../models/violation.dart';
|
|
6
|
+
|
|
7
|
+
/// Base class for all rule analyzers
|
|
8
|
+
abstract class BaseAnalyzer {
|
|
9
|
+
/// Rule ID this analyzer handles
|
|
10
|
+
String get ruleId;
|
|
11
|
+
|
|
12
|
+
/// Analyze a compilation unit and return violations
|
|
13
|
+
List<Violation> analyze({
|
|
14
|
+
required CompilationUnit unit,
|
|
15
|
+
required String filePath,
|
|
16
|
+
required Rule rule,
|
|
17
|
+
required LineInfo lineInfo,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
/// Get line number from offset
|
|
21
|
+
int getLine(LineInfo lineInfo, int offset) {
|
|
22
|
+
return lineInfo.getLocation(offset).lineNumber;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Get column from offset
|
|
26
|
+
int getColumn(LineInfo lineInfo, int offset) {
|
|
27
|
+
return lineInfo.getLocation(offset).columnNumber;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Create a violation
|
|
31
|
+
Violation createViolation({
|
|
32
|
+
required String filePath,
|
|
33
|
+
required int line,
|
|
34
|
+
required int column,
|
|
35
|
+
required String message,
|
|
36
|
+
String severity = 'warning',
|
|
37
|
+
int? confidence,
|
|
38
|
+
Map<String, dynamic>? metadata,
|
|
39
|
+
}) {
|
|
40
|
+
return Violation(
|
|
41
|
+
ruleId: ruleId,
|
|
42
|
+
filePath: filePath,
|
|
43
|
+
line: line,
|
|
44
|
+
column: column,
|
|
45
|
+
message: message,
|
|
46
|
+
severity: severity,
|
|
47
|
+
analysisMethod: 'ast',
|
|
48
|
+
confidence: confidence,
|
|
49
|
+
metadata: metadata,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
3
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
4
|
+
import 'package:analyzer/source/line_info.dart';
|
|
5
|
+
import 'package:crypto/crypto.dart';
|
|
6
|
+
|
|
7
|
+
import '../../models/rule.dart';
|
|
8
|
+
import '../../models/violation.dart';
|
|
9
|
+
import '../base_analyzer.dart';
|
|
10
|
+
|
|
11
|
+
/// C002: No Duplicate Code (> 10 lines)
|
|
12
|
+
/// Detects duplicate code blocks that should be refactored
|
|
13
|
+
/// - Finds functions/methods with similar bodies
|
|
14
|
+
/// - Detects copy-pasted code blocks
|
|
15
|
+
/// - Suggests extracting common logic
|
|
16
|
+
class C002NoDuplicateCodeAnalyzer extends BaseAnalyzer {
|
|
17
|
+
@override
|
|
18
|
+
String get ruleId => 'C002';
|
|
19
|
+
|
|
20
|
+
/// Minimum lines for duplicate detection
|
|
21
|
+
static const int minDuplicateLines = 10;
|
|
22
|
+
|
|
23
|
+
/// Similarity threshold (0.0 - 1.0)
|
|
24
|
+
static const double similarityThreshold = 0.85;
|
|
25
|
+
|
|
26
|
+
/// Store code blocks for cross-file comparison
|
|
27
|
+
final Map<String, List<_CodeBlock>> _codeBlocks = {};
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
List<Violation> analyze({
|
|
31
|
+
required CompilationUnit unit,
|
|
32
|
+
required String filePath,
|
|
33
|
+
required Rule rule,
|
|
34
|
+
required LineInfo lineInfo,
|
|
35
|
+
}) {
|
|
36
|
+
final violations = <Violation>[];
|
|
37
|
+
|
|
38
|
+
final visitor = _DuplicateCodeVisitor(
|
|
39
|
+
filePath: filePath,
|
|
40
|
+
lineInfo: lineInfo,
|
|
41
|
+
violations: violations,
|
|
42
|
+
analyzer: this,
|
|
43
|
+
codeBlocks: _codeBlocks,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
unit.accept(visitor);
|
|
47
|
+
|
|
48
|
+
// Check for duplicates within collected blocks (only for current file)
|
|
49
|
+
_detectDuplicates(violations, filePath);
|
|
50
|
+
|
|
51
|
+
return violations;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void _detectDuplicates(List<Violation> violations, String currentFilePath) {
|
|
55
|
+
final allBlocks = _codeBlocks.values.expand((b) => b).toList();
|
|
56
|
+
final reportedPairs = <String>{};
|
|
57
|
+
|
|
58
|
+
// Performance: Get only blocks from current file to compare against others
|
|
59
|
+
final currentFileBlocks = _codeBlocks[currentFilePath] ?? [];
|
|
60
|
+
if (currentFileBlocks.isEmpty) return;
|
|
61
|
+
|
|
62
|
+
// Group blocks by hash for faster exact match lookups
|
|
63
|
+
final blocksByHash = <String, List<_CodeBlock>>{};
|
|
64
|
+
for (final block in allBlocks) {
|
|
65
|
+
blocksByHash.putIfAbsent(block.hash, () => []).add(block);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (final currentBlock in currentFileBlocks) {
|
|
69
|
+
// Fast path: Check exact duplicates by hash first
|
|
70
|
+
final sameHashBlocks = blocksByHash[currentBlock.hash] ?? [];
|
|
71
|
+
for (final otherBlock in sameHashBlocks) {
|
|
72
|
+
// Skip self or same file overlapping blocks
|
|
73
|
+
if (otherBlock.filePath == currentFilePath) {
|
|
74
|
+
if (otherBlock == currentBlock || _overlaps(currentBlock, otherBlock)) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
final pairKey = _createPairKey(currentBlock, otherBlock);
|
|
80
|
+
if (reportedPairs.contains(pairKey)) continue;
|
|
81
|
+
|
|
82
|
+
reportedPairs.add(pairKey);
|
|
83
|
+
violations.add(createViolation(
|
|
84
|
+
filePath: currentBlock.filePath,
|
|
85
|
+
line: currentBlock.startLine,
|
|
86
|
+
column: 1,
|
|
87
|
+
message:
|
|
88
|
+
'Duplicate code block found (${currentBlock.lineCount} lines) - same as ${otherBlock.filePath}:${otherBlock.startLine}',
|
|
89
|
+
severity: 'warning',
|
|
90
|
+
confidence: 100,
|
|
91
|
+
metadata: {
|
|
92
|
+
'duplicateOf': '${otherBlock.filePath}:${otherBlock.startLine}',
|
|
93
|
+
'lineCount': currentBlock.lineCount,
|
|
94
|
+
'type': currentBlock.type,
|
|
95
|
+
'similarity': 1.0,
|
|
96
|
+
},
|
|
97
|
+
));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Slow path: Check similar code (skip if already found exact match)
|
|
101
|
+
if (sameHashBlocks.length <= 1) {
|
|
102
|
+
for (final otherBlock in allBlocks) {
|
|
103
|
+
// Skip same file, self, or different file
|
|
104
|
+
if (otherBlock.filePath == currentFilePath) {
|
|
105
|
+
if (otherBlock == currentBlock || _overlaps(currentBlock, otherBlock)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Skip if not enough lines
|
|
111
|
+
if (currentBlock.lineCount < minDuplicateLines ||
|
|
112
|
+
otherBlock.lineCount < minDuplicateLines) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
final pairKey = _createPairKey(currentBlock, otherBlock);
|
|
117
|
+
if (reportedPairs.contains(pairKey)) continue;
|
|
118
|
+
|
|
119
|
+
final similarity = _calculateSimilarity(currentBlock, otherBlock);
|
|
120
|
+
if (similarity >= similarityThreshold && similarity < 1.0) {
|
|
121
|
+
reportedPairs.add(pairKey);
|
|
122
|
+
violations.add(createViolation(
|
|
123
|
+
filePath: currentBlock.filePath,
|
|
124
|
+
line: currentBlock.startLine,
|
|
125
|
+
column: 1,
|
|
126
|
+
message:
|
|
127
|
+
'Similar code block found (${(similarity * 100).toStringAsFixed(0)}% similar) - consider extracting common logic from ${otherBlock.filePath}:${otherBlock.startLine}',
|
|
128
|
+
severity: 'warning',
|
|
129
|
+
confidence: (similarity * 100).toInt(),
|
|
130
|
+
metadata: {
|
|
131
|
+
'similarTo': '${otherBlock.filePath}:${otherBlock.startLine}',
|
|
132
|
+
'lineCount': currentBlock.lineCount,
|
|
133
|
+
'type': currentBlock.type,
|
|
134
|
+
'similarity': similarity,
|
|
135
|
+
},
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Create unique pair key to avoid duplicate reporting
|
|
144
|
+
String _createPairKey(_CodeBlock a, _CodeBlock b) {
|
|
145
|
+
final key1 = '${a.filePath}:${a.startLine}';
|
|
146
|
+
final key2 = '${b.filePath}:${b.startLine}';
|
|
147
|
+
// Sort keys to ensure consistent ordering
|
|
148
|
+
return key1.compareTo(key2) < 0 ? '$key1|$key2' : '$key2|$key1';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
bool _overlaps(_CodeBlock a, _CodeBlock b) {
|
|
152
|
+
return !(a.endLine < b.startLine || b.endLine < a.startLine);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
double _calculateSimilarity(_CodeBlock a, _CodeBlock b) {
|
|
156
|
+
final lines1 = a.normalizedLines;
|
|
157
|
+
final lines2 = b.normalizedLines;
|
|
158
|
+
|
|
159
|
+
if (lines1.isEmpty || lines2.isEmpty) return 0.0;
|
|
160
|
+
|
|
161
|
+
// Quick size difference check - if too different, skip expensive calculations
|
|
162
|
+
final maxLength = lines1.length > lines2.length ? lines1.length : lines2.length;
|
|
163
|
+
final minLength = lines1.length < lines2.length ? lines1.length : lines2.length;
|
|
164
|
+
final sizeDiff = (maxLength - minLength) / maxLength;
|
|
165
|
+
if (sizeDiff > 0.3) return 0.0; // More than 30% size difference
|
|
166
|
+
|
|
167
|
+
// Fast path: Line-by-line exact match similarity (cheapest)
|
|
168
|
+
var matchingLines = 0;
|
|
169
|
+
for (var i = 0; i < minLength; i++) {
|
|
170
|
+
if (lines1[i] == lines2[i]) {
|
|
171
|
+
matchingLines++;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
final exactSimilarity = matchingLines / maxLength;
|
|
175
|
+
|
|
176
|
+
// Early return if exact similarity is already high enough
|
|
177
|
+
if (exactSimilarity >= similarityThreshold) {
|
|
178
|
+
return exactSimilarity;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Medium cost: Token-based similarity (Jaccard)
|
|
182
|
+
final tokens1 = lines1.join(' ').split(RegExp(r'\s+'));
|
|
183
|
+
final tokens2 = lines2.join(' ').split(RegExp(r'\s+'));
|
|
184
|
+
final set1 = tokens1.toSet();
|
|
185
|
+
final set2 = tokens2.toSet();
|
|
186
|
+
final intersection = set1.intersection(set2).length;
|
|
187
|
+
final union = set1.union(set2).length;
|
|
188
|
+
final jaccardSimilarity = union > 0 ? intersection / union : 0.0;
|
|
189
|
+
|
|
190
|
+
// Early return if Jaccard is high enough
|
|
191
|
+
if (jaccardSimilarity >= similarityThreshold) {
|
|
192
|
+
return jaccardSimilarity;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Most expensive: LCS only if needed
|
|
196
|
+
final lcsLength = _lcsLength(lines1, lines2);
|
|
197
|
+
final lcsSimilarity = lcsLength / maxLength;
|
|
198
|
+
|
|
199
|
+
// Return the maximum similarity
|
|
200
|
+
return [exactSimilarity, jaccardSimilarity, lcsSimilarity]
|
|
201
|
+
.reduce((a, b) => a > b ? a : b);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
int _lcsLength(List<String> a, List<String> b) {
|
|
205
|
+
final m = a.length;
|
|
206
|
+
final n = b.length;
|
|
207
|
+
final dp = List.generate(m + 1, (_) => List.filled(n + 1, 0));
|
|
208
|
+
|
|
209
|
+
for (var i = 1; i <= m; i++) {
|
|
210
|
+
for (var j = 1; j <= n; j++) {
|
|
211
|
+
if (a[i - 1] == b[j - 1]) {
|
|
212
|
+
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
213
|
+
} else {
|
|
214
|
+
dp[i][j] = dp[i - 1][j] > dp[i][j - 1] ? dp[i - 1][j] : dp[i][j - 1];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return dp[m][n];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/// Clear collected blocks (call between analysis sessions)
|
|
223
|
+
void reset() {
|
|
224
|
+
_codeBlocks.clear();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class _CodeBlock {
|
|
229
|
+
final String filePath;
|
|
230
|
+
final int startLine;
|
|
231
|
+
final int endLine;
|
|
232
|
+
final String hash;
|
|
233
|
+
final List<String> normalizedLines;
|
|
234
|
+
final String type;
|
|
235
|
+
|
|
236
|
+
_CodeBlock({
|
|
237
|
+
required this.filePath,
|
|
238
|
+
required this.startLine,
|
|
239
|
+
required this.endLine,
|
|
240
|
+
required this.hash,
|
|
241
|
+
required this.normalizedLines,
|
|
242
|
+
required this.type,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
int get lineCount => endLine - startLine + 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
class _DuplicateCodeVisitor extends RecursiveAstVisitor<void> {
|
|
249
|
+
final String filePath;
|
|
250
|
+
final LineInfo lineInfo;
|
|
251
|
+
final List<Violation> violations;
|
|
252
|
+
final C002NoDuplicateCodeAnalyzer analyzer;
|
|
253
|
+
final Map<String, List<_CodeBlock>> codeBlocks;
|
|
254
|
+
|
|
255
|
+
_DuplicateCodeVisitor({
|
|
256
|
+
required this.filePath,
|
|
257
|
+
required this.lineInfo,
|
|
258
|
+
required this.violations,
|
|
259
|
+
required this.analyzer,
|
|
260
|
+
required this.codeBlocks,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
@override
|
|
264
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
265
|
+
final body = node.functionExpression.body;
|
|
266
|
+
if (body is BlockFunctionBody) {
|
|
267
|
+
_processBlock(body.block, 'function', node.name.lexeme);
|
|
268
|
+
}
|
|
269
|
+
super.visitFunctionDeclaration(node);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@override
|
|
273
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
274
|
+
final body = node.body;
|
|
275
|
+
if (body is BlockFunctionBody) {
|
|
276
|
+
_processBlock(body.block, 'method', node.name.lexeme);
|
|
277
|
+
}
|
|
278
|
+
super.visitMethodDeclaration(node);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
void _processBlock(Block block, String type, String name) {
|
|
282
|
+
final startLine = analyzer.getLine(lineInfo, block.offset);
|
|
283
|
+
final endLine = analyzer.getLine(lineInfo, block.end);
|
|
284
|
+
final lineCount = endLine - startLine + 1;
|
|
285
|
+
|
|
286
|
+
if (lineCount < C002NoDuplicateCodeAnalyzer.minDuplicateLines) return;
|
|
287
|
+
|
|
288
|
+
// Normalize code for comparison
|
|
289
|
+
final normalizedLines = _normalizeBlock(block);
|
|
290
|
+
final hash = _hashLines(normalizedLines);
|
|
291
|
+
|
|
292
|
+
final codeBlock = _CodeBlock(
|
|
293
|
+
filePath: filePath,
|
|
294
|
+
startLine: startLine,
|
|
295
|
+
endLine: endLine,
|
|
296
|
+
hash: hash,
|
|
297
|
+
normalizedLines: normalizedLines,
|
|
298
|
+
type: '$type:$name',
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
codeBlocks.putIfAbsent(filePath, () => []).add(codeBlock);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
List<String> _normalizeBlock(Block block) {
|
|
305
|
+
final source = block.toSource();
|
|
306
|
+
return source
|
|
307
|
+
.split('\n')
|
|
308
|
+
.map((line) {
|
|
309
|
+
// Trim whitespace
|
|
310
|
+
var normalized = line.trim();
|
|
311
|
+
|
|
312
|
+
// Skip empty lines and comments
|
|
313
|
+
if (normalized.isEmpty ||
|
|
314
|
+
normalized.startsWith('//') ||
|
|
315
|
+
normalized.startsWith('/*')) {
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Normalize string literals (preserve quotes for structure)
|
|
320
|
+
normalized = normalized
|
|
321
|
+
.replaceAll(RegExp(r"'[^']*'"), "'_STR_'")
|
|
322
|
+
.replaceAll(RegExp(r'"[^"]*"'), '"_STR_"');
|
|
323
|
+
|
|
324
|
+
// Normalize numbers
|
|
325
|
+
normalized = normalized.replaceAll(RegExp(r'\b\d+\.?\d*\b'), '_NUM_');
|
|
326
|
+
|
|
327
|
+
// Normalize variable/parameter names (but keep method calls)
|
|
328
|
+
// Replace local variable patterns but preserve structure
|
|
329
|
+
normalized = normalized
|
|
330
|
+
.replaceAll(RegExp(r'\bfinal\s+\w+\s+='), 'final _var_ =')
|
|
331
|
+
.replaceAll(RegExp(r'\bvar\s+\w+\s+='), 'var _var_ =')
|
|
332
|
+
.replaceAll(RegExp(r'\b(\w+)\s*=\s*'), '_var_ = ');
|
|
333
|
+
|
|
334
|
+
return normalized;
|
|
335
|
+
})
|
|
336
|
+
.where((line) => line.isNotEmpty)
|
|
337
|
+
.toList();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
String _hashLines(List<String> lines) {
|
|
341
|
+
final content = lines.join('\n');
|
|
342
|
+
return md5.convert(utf8.encode(content)).toString();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// C003: No Vague Abbreviations
|
|
10
|
+
/// Ensures clear, understandable variable names without arbitrary abbreviations
|
|
11
|
+
/// - Detects unclear abbreviations (e.g., tmp, val, obj)
|
|
12
|
+
/// - Allows common well-known abbreviations (id, url, api)
|
|
13
|
+
/// - Enforces minimum name length
|
|
14
|
+
class C003NoVagueAbbreviationsAnalyzer extends BaseAnalyzer {
|
|
15
|
+
@override
|
|
16
|
+
String get ruleId => 'C003';
|
|
17
|
+
|
|
18
|
+
/// Minimum variable name length
|
|
19
|
+
static const int minNameLength = 2;
|
|
20
|
+
|
|
21
|
+
/// Allowed single character names (loop variables, etc.)
|
|
22
|
+
static final Set<String> allowedSingleChar = {
|
|
23
|
+
'i', 'j', 'k', // Loop indices
|
|
24
|
+
'x', 'y', 'z', // Coordinates
|
|
25
|
+
'e', // Exception
|
|
26
|
+
'_', // Unused
|
|
27
|
+
'a', 'b', // Generic in math
|
|
28
|
+
'n', 'm', // Count/size
|
|
29
|
+
't', // Time/temp
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/// Well-known abbreviations that are allowed
|
|
33
|
+
static final Set<String> allowedAbbreviations = {
|
|
34
|
+
'id',
|
|
35
|
+
'Id',
|
|
36
|
+
'ID',
|
|
37
|
+
'url',
|
|
38
|
+
'Url',
|
|
39
|
+
'URL',
|
|
40
|
+
'api',
|
|
41
|
+
'Api',
|
|
42
|
+
'API',
|
|
43
|
+
'io',
|
|
44
|
+
'IO',
|
|
45
|
+
'db',
|
|
46
|
+
'DB',
|
|
47
|
+
'ui',
|
|
48
|
+
'UI',
|
|
49
|
+
'ok',
|
|
50
|
+
'OK',
|
|
51
|
+
'http',
|
|
52
|
+
'HTTP',
|
|
53
|
+
'json',
|
|
54
|
+
'JSON',
|
|
55
|
+
'xml',
|
|
56
|
+
'XML',
|
|
57
|
+
'html',
|
|
58
|
+
'HTML',
|
|
59
|
+
'css',
|
|
60
|
+
'CSS',
|
|
61
|
+
'sql',
|
|
62
|
+
'SQL',
|
|
63
|
+
'tcp',
|
|
64
|
+
'TCP',
|
|
65
|
+
'udp',
|
|
66
|
+
'UDP',
|
|
67
|
+
'ip',
|
|
68
|
+
'IP',
|
|
69
|
+
'os',
|
|
70
|
+
'OS',
|
|
71
|
+
'gc',
|
|
72
|
+
'GC',
|
|
73
|
+
'vm',
|
|
74
|
+
'VM',
|
|
75
|
+
'min',
|
|
76
|
+
'max',
|
|
77
|
+
'src',
|
|
78
|
+
'dst',
|
|
79
|
+
'err',
|
|
80
|
+
'msg',
|
|
81
|
+
'req',
|
|
82
|
+
'res',
|
|
83
|
+
'ctx',
|
|
84
|
+
'cfg',
|
|
85
|
+
'env',
|
|
86
|
+
'idx',
|
|
87
|
+
'len',
|
|
88
|
+
'num',
|
|
89
|
+
'str',
|
|
90
|
+
'int',
|
|
91
|
+
'ptr',
|
|
92
|
+
'ref',
|
|
93
|
+
'btn',
|
|
94
|
+
'img',
|
|
95
|
+
'doc',
|
|
96
|
+
'dir',
|
|
97
|
+
'tmp', // Allow tmp in some contexts
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/// Vague abbreviations that should be avoided
|
|
101
|
+
static final Set<String> vagueAbbreviations = {
|
|
102
|
+
'val',
|
|
103
|
+
'obj',
|
|
104
|
+
'data',
|
|
105
|
+
'info',
|
|
106
|
+
'item',
|
|
107
|
+
'thing',
|
|
108
|
+
'stuff',
|
|
109
|
+
'foo',
|
|
110
|
+
'bar',
|
|
111
|
+
'baz',
|
|
112
|
+
'qux',
|
|
113
|
+
'temp',
|
|
114
|
+
'tmp2',
|
|
115
|
+
'var1',
|
|
116
|
+
'var2',
|
|
117
|
+
'str1',
|
|
118
|
+
'str2',
|
|
119
|
+
'num1',
|
|
120
|
+
'num2',
|
|
121
|
+
'test1',
|
|
122
|
+
'test2',
|
|
123
|
+
'xyz',
|
|
124
|
+
'abc',
|
|
125
|
+
'aaa',
|
|
126
|
+
'bbb',
|
|
127
|
+
'ccc',
|
|
128
|
+
'xxx',
|
|
129
|
+
'yyy',
|
|
130
|
+
'zzz',
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
List<Violation> analyze({
|
|
135
|
+
required CompilationUnit unit,
|
|
136
|
+
required String filePath,
|
|
137
|
+
required Rule rule,
|
|
138
|
+
required LineInfo lineInfo,
|
|
139
|
+
}) {
|
|
140
|
+
final violations = <Violation>[];
|
|
141
|
+
|
|
142
|
+
final visitor = _AbbreviationVisitor(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
lineInfo: lineInfo,
|
|
145
|
+
violations: violations,
|
|
146
|
+
analyzer: this,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
unit.accept(visitor);
|
|
150
|
+
|
|
151
|
+
return violations;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Check if a name is a vague abbreviation
|
|
155
|
+
static bool isVagueAbbreviation(String name) {
|
|
156
|
+
final lowerName = name.toLowerCase().replaceAll('_', '');
|
|
157
|
+
return vagueAbbreviations.contains(lowerName);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Check if name is allowed despite being short
|
|
161
|
+
static bool isAllowedShortName(String name) {
|
|
162
|
+
// Remove leading underscores for private members
|
|
163
|
+
final cleanName = name.replaceAll(RegExp(r'^_+'), '');
|
|
164
|
+
|
|
165
|
+
if (cleanName.length == 1) {
|
|
166
|
+
return allowedSingleChar.contains(cleanName.toLowerCase());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return allowedAbbreviations.contains(cleanName) ||
|
|
170
|
+
allowedAbbreviations.contains(cleanName.toLowerCase());
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Check if name looks like meaningless pattern (x1, y2, temp1, etc.)
|
|
174
|
+
static bool isMeaninglessPattern(String name) {
|
|
175
|
+
final cleanName = name.replaceAll(RegExp(r'^_+'), '');
|
|
176
|
+
|
|
177
|
+
// Single letter followed by numbers
|
|
178
|
+
if (RegExp(r'^[a-zA-Z]\d+$').hasMatch(cleanName)) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Common meaningless patterns
|
|
183
|
+
if (RegExp(r'^(temp|tmp|var|val|obj|data|item)\d*$', caseSensitive: false)
|
|
184
|
+
.hasMatch(cleanName)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class _AbbreviationVisitor extends RecursiveAstVisitor<void> {
|
|
193
|
+
final String filePath;
|
|
194
|
+
final LineInfo lineInfo;
|
|
195
|
+
final List<Violation> violations;
|
|
196
|
+
final C003NoVagueAbbreviationsAnalyzer analyzer;
|
|
197
|
+
|
|
198
|
+
_AbbreviationVisitor({
|
|
199
|
+
required this.filePath,
|
|
200
|
+
required this.lineInfo,
|
|
201
|
+
required this.violations,
|
|
202
|
+
required this.analyzer,
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
@override
|
|
206
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
207
|
+
_checkName(node.name.lexeme, node.name.offset, 'Variable');
|
|
208
|
+
super.visitVariableDeclaration(node);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
213
|
+
_checkName(node.name.lexeme, node.name.offset, 'Function');
|
|
214
|
+
super.visitFunctionDeclaration(node);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
@override
|
|
218
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
219
|
+
// Skip operators
|
|
220
|
+
if (node.isOperator) return;
|
|
221
|
+
_checkName(node.name.lexeme, node.name.offset, 'Method');
|
|
222
|
+
super.visitMethodDeclaration(node);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
@override
|
|
226
|
+
void visitSimpleFormalParameter(SimpleFormalParameter node) {
|
|
227
|
+
final name = node.name?.lexeme;
|
|
228
|
+
if (name != null) {
|
|
229
|
+
_checkName(name, node.name!.offset, 'Parameter');
|
|
230
|
+
}
|
|
231
|
+
super.visitSimpleFormalParameter(node);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@override
|
|
235
|
+
void visitFieldFormalParameter(FieldFormalParameter node) {
|
|
236
|
+
_checkName(node.name.lexeme, node.name.offset, 'Parameter');
|
|
237
|
+
super.visitFieldFormalParameter(node);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
void _checkName(String name, int offset, String type) {
|
|
241
|
+
// Skip private prefix for checking
|
|
242
|
+
final cleanName = name.replaceAll(RegExp(r'^_+'), '');
|
|
243
|
+
|
|
244
|
+
// Skip if empty after cleaning
|
|
245
|
+
if (cleanName.isEmpty) return;
|
|
246
|
+
|
|
247
|
+
// Check for single character (unless allowed)
|
|
248
|
+
if (cleanName.length == 1 &&
|
|
249
|
+
!C003NoVagueAbbreviationsAnalyzer.allowedSingleChar
|
|
250
|
+
.contains(cleanName.toLowerCase())) {
|
|
251
|
+
violations.add(analyzer.createViolation(
|
|
252
|
+
filePath: filePath,
|
|
253
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
254
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
255
|
+
message:
|
|
256
|
+
'$type name "$name" is too short - use a descriptive name',
|
|
257
|
+
metadata: {
|
|
258
|
+
'name': name,
|
|
259
|
+
'type': type.toLowerCase(),
|
|
260
|
+
'issue': 'too_short',
|
|
261
|
+
},
|
|
262
|
+
));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for vague abbreviations
|
|
267
|
+
if (C003NoVagueAbbreviationsAnalyzer.isVagueAbbreviation(cleanName)) {
|
|
268
|
+
violations.add(analyzer.createViolation(
|
|
269
|
+
filePath: filePath,
|
|
270
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
271
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
272
|
+
message:
|
|
273
|
+
'$type name "$name" is too vague - use a more descriptive name',
|
|
274
|
+
metadata: {
|
|
275
|
+
'name': name,
|
|
276
|
+
'type': type.toLowerCase(),
|
|
277
|
+
'issue': 'vague_abbreviation',
|
|
278
|
+
},
|
|
279
|
+
));
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check for meaningless patterns
|
|
284
|
+
if (C003NoVagueAbbreviationsAnalyzer.isMeaninglessPattern(cleanName)) {
|
|
285
|
+
violations.add(analyzer.createViolation(
|
|
286
|
+
filePath: filePath,
|
|
287
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
288
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
289
|
+
message:
|
|
290
|
+
'$type name "$name" follows a meaningless pattern - use a descriptive name',
|
|
291
|
+
metadata: {
|
|
292
|
+
'name': name,
|
|
293
|
+
'type': type.toLowerCase(),
|
|
294
|
+
'issue': 'meaningless_pattern',
|
|
295
|
+
},
|
|
296
|
+
));
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check minimum length for non-allowed abbreviations
|
|
301
|
+
if (cleanName.length < C003NoVagueAbbreviationsAnalyzer.minNameLength &&
|
|
302
|
+
!C003NoVagueAbbreviationsAnalyzer.isAllowedShortName(cleanName)) {
|
|
303
|
+
violations.add(analyzer.createViolation(
|
|
304
|
+
filePath: filePath,
|
|
305
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
306
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
307
|
+
message:
|
|
308
|
+
'$type name "$name" is too short (min ${C003NoVagueAbbreviationsAnalyzer.minNameLength} chars) - use a descriptive name',
|
|
309
|
+
metadata: {
|
|
310
|
+
'name': name,
|
|
311
|
+
'type': type.toLowerCase(),
|
|
312
|
+
'issue': 'too_short',
|
|
313
|
+
'minLength': C003NoVagueAbbreviationsAnalyzer.minNameLength,
|
|
314
|
+
},
|
|
315
|
+
));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|