refactor: migrate function_lines_of_code#295
Conversation
* Add analysis_server_plugin dependency * Migrate avoid_global_state rule and tests * applied code review suggestions * applied suggestions from code review * edited main documentation and changed avoid_global_state visitor naming
* migrated avoid non null assertion rule and tests * applied code review suggestions * applied code review suggestions regarding making lintCode private
* Add analysis_server_plugin dependency * Migrate avoid_global_state rule and tests * applied code review suggestions * applied code review suggestions * applied suggestions from code review * migrated avoid debug print in release and tests * edited main documentation and changed avoid_global_state visitor naming * fixed main.dart conflict * applied suggestions from review * refactored code according to suggestions from review * applied code review suggestions * applied latest changes according to code review * remove unnecessary comments in AvoidDebugPrintInReleaseVisitor
* Migrate avoid_global_state rule and tests * applied code review suggestions * migrated avoid debug print in release and tests * applied suggestions from code review * edited main documentation and changed avoid_global_state visitor naming * migrated proper super calls rule and tests * registered ProperSuperCallsRule in the Plugin * applied code review suggestions * Fix tests structure * Remove unnecessary file --------- Co-authored-by: vova-beloded-solid <vova.beloded@solid.software>
* Updated prefer_early_return rule and its visitor to use AnalysisRule . * Added tests and fixed visitor for prefer early return * Added the suggested changes
…oftware#237) * Updated avoid_unnecessary_return_variable rule and its visitor to use AnalysisRule. * Updated tests for avoid_unnecessary_return_variable rule * Deleted previous test file for avoid_unnecessary_return_variable rule. * Updated ReturnVariableUsageVisitor description. * Updated avoid_unnecessary_return_variable visitor from recursive to simple visitor. * Address PR suggestions * Updated rule to private in the visitor.
…e#238) * Updated avoid_unnecessary_set_state rule and its visitor to use AnalysisRule. * Fixed previous commit. * Updated tests for avoid_unnecessary_set_state rule. * Delete previous test file for avoid_unnecessary_set_state rule * Updated the avoid_unnecessary_set_state visitor from recursive to simple. * Removed unnecessary set up from avoid_unnecessary_set_state rule tests * Update lib/src/lints/avoid_unnecessary_setstate/avoid_unnecessary_set_state_rule.dart Co-authored-by: danylo-safonov-solid <116712879+danylo-safonov-solid@users.noreply.github.com> * Address PR suggestions * Updated sdk requirements * Updated rule to private in visitor --------- Co-authored-by: danylo-safonov-solid <116712879+danylo-safonov-solid@users.noreply.github.com>
* migrate prefer early return rule with tests * fix: remove flutter dependency to allow running reflective tests * test: migrate all old tests * feat: migrate old lint code improve readability when possible * refactor: extract variable for readability * style: formatting * test: remove unnecessary test file test: add nested 3 ifs test case --------- Co-authored-by: Andrew Bekhiet <andrew.bekhiet@solid.software>
…#239) * Updated avoid_unrelated_type_assertions and its visitor to use AnalysisRule. * Updated tests for avoid_unrelated_type_assertions rule * Delete previous test file for avoid_unrelated_type_assertions * Update variable modifiers to private * Updated avoid_unrelared_type_assertions visitor from recursive to simple
* Updated newline_before_return_visitor rule and its visitor * Merged tests for newline_return_rule * Delete previous test file for newline_before_return * Updated min version of sdk * Added safety handle for the null case * Update newline_before_return_rule_test.dart * Added test for multiple comments and for a newline before a comment.
* Updated no_equal_then_else rule and its visitor * Merged tests for no_equal_then_else rule * Delete previous test file for no_equal_then_else * Applied PR suggestions from Gemini
…lid-software#281) * refactor: migrate double_literal_format to analysis_server_plugin docs: improve code docs * fix: handle literals with capital E test: handle exponentials in test cases * fix: include unnecessary leading zeroes in lint description
* Created analysis_options.yaml rules parser * Improved yaml parser and added the analysis_options loader * Improved rules loader from yaml * Added verification before looking for .yaml's path * Fields and getters are now declared before the constructor * Added method to get options of a rule by it's name * Made suggested changes to file upward finder * Removed top-level variable * Improved name of variable in loadRuleFromContext * Updated analysis options to have rules for each configuration file path * Updated file upward finder to not mix File from dart.io with file from analyzer * Added usage example in avoid_global_state_rule * style: move getters and fields before constructor * style: improve readability * fix: don't parse enabled if the rule has configured options style: improve variable names refactor: use root package path instead of library path refactor: use pattern matching to reduce nesting * feat: reload rules from file if newer refactor: allow mocking resource provider refactor: extract CachedPackageRules model * test: add AnalysisOptionsLoaderTest * feat(SolidLintRule): add parameter parsing refactor: use Map<String, Object?> instead of LintOptions as the enabled field is implicitly true for all rules that the analyzer processes remove RuleConfig as it is no longer needed * fix: use Map<String, Object?> for raw rule config * fix: method name * fix: make sure rules options are loaded before getting parameters * refactor: remove unused AnalysisOptionsLoader from AvoidGlobalStateRule refactor: use for loop to register rules * refactor: extract duplicate logic --------- Co-authored-by: Andrew Bekhiet <andrew.bekhiet@solid.software>
* feat: migrate avoid_final_with_getter refactor: extract GetterVariableVisitor into a file refactor: extract GetterReferenceId into a file test(avoid_final_with_getter): migrate tests * fix: also check block getters with one return statements fix: also check getter returning private final fields with this keyword * fix: also rename private field references
feat: also check FunctionDeclarationStatement
* refactor: migrate unnecessary_type_assertion * refactor: migrate unnecessary_type_assertion fix * fix: use fix kind instead of assist kind * fix: use thisOrAncestorOfType to get the nearest node of type T fix: remove leading space when removing is operator * feat: display removed part in the fix message refactor: simplify removal code * fix: handle custom iterable types * test: add test for custom iterables with subtypes * refactor: improve readability * fix: improve AutoTestLintOffsets reliability handles edge cases when there are multiple similar code fragments
There was a problem hiding this comment.
Code Review
This pull request migrates the solid_lints package from custom_lint_builder to the new analysis_server_plugin API, refactoring various lint rules as AnalysisRule subclasses and adding comprehensive reflective tests. The review feedback highlights several critical issues: a correctness bug in _isWrappedInReleaseCheck that fails to verify which branch of an IfStatement contains the debugPrint call; a pattern matching failure in UnnecessaryWhereTypeVisitor for implicit calls on this which can be resolved by using realTarget; a potential syntax error in AvoidUnnecessaryTypeAssertionsFix when whereType is called implicitly; and a directory traversal bug in _findNearestAnalysisOptionsFilePath that misses checking the root directory for analysis_options.yaml.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| bool _isWrappedInReleaseCheck(AstNode node) { | ||
| AstNode? parent = node.parent; | ||
|
|
||
| while (parent != null) { | ||
| if (parent is IfStatement) { | ||
| final condition = parent.expression; | ||
|
|
||
| if (_isNotReleaseModeCheck(condition) || _isDebugModeCheck(condition)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| parent = parent.parent; | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
There is a correctness bug in _isWrappedInReleaseCheck. It traverses up the AST to find any ancestor IfStatement with a !kReleaseMode or kDebugMode condition, but it does not verify whether the debugPrint call is actually inside the then branch or the else branch of that IfStatement.
If a debugPrint is placed inside the else branch of if (!kReleaseMode), it will run in release mode, but this visitor will incorrectly treat it as safe and fail to report it.
To fix this, we should track the child node as we traverse up and ensure the check is only considered safe if the node is in the correct branch.
bool _isWrappedInReleaseCheck(AstNode node) {
AstNode? current = node;
AstNode? parent = node.parent;
while (parent != null) {
if (parent is IfStatement) {
final condition = parent.expression;
if (current == parent.thenStatement) {
if (_isNotReleaseModeCheck(condition) || _isDebugModeCheck(condition)) {
return true;
}
} else if (current == parent.elseStatement) {
if (_isReleaseModeCheck(condition)) {
return true;
}
}
}
current = parent;
parent = parent.parent;
}
return false;
}
bool _isReleaseModeCheck(Expression condition) {
return condition is SimpleIdentifier && condition.name == _kReleaseMode;
}| bool _isUnnecessaryWhereType(MethodInvocation node) { | ||
| if (node case MethodInvocation( | ||
| methodName: Identifier( | ||
| name: AvoidUnnecessaryTypeAssertionsRule.whereTypeMethodName, | ||
| ), | ||
| target: Expression(staticType: final InterfaceType targetType), | ||
| typeArguments: TypeArgumentList(:final arguments), | ||
| ) when arguments.isNotEmpty) { | ||
| final targetIterableType = switch (targetType) { | ||
| InterfaceType(isDartCoreIterable: true) => targetType, | ||
| InterfaceType(:final allSupertypes) => allSupertypes.firstWhereOrNull( | ||
| (e) => e.isDartCoreIterable, | ||
| ), | ||
| }; | ||
|
|
||
| final objectType = targetIterableType?.typeArguments.firstOrNull; | ||
| final castedType = arguments.first.type; | ||
|
|
||
| if (castedType == null || objectType == null) { | ||
| return false; | ||
| } | ||
|
|
||
| final typeCast = TypeCast(source: objectType, target: castedType); | ||
|
|
||
| return typeCast.isUnnecessaryTypeCheck; | ||
| } | ||
|
|
||
| return false; | ||
| } |
There was a problem hiding this comment.
Using target: Expression(...) in the pattern match will fail to match when whereType is called implicitly on this (i.e., without an explicit target, like whereType<double>() inside an Iterable subclass), because target is null in the AST for implicit calls.
To fix this and avoid false negatives, we should match on realTarget instead of target, as realTarget is never null and correctly resolves to the implicit this expression.
bool _isUnnecessaryWhereType(MethodInvocation node) {
if (node case MethodInvocation(
methodName: Identifier(
name: AvoidUnnecessaryTypeAssertionsRule.whereTypeMethodName,
),
realTarget: Expression(staticType: final InterfaceType targetType),
typeArguments: TypeArgumentList(:final arguments),
) when arguments.isNotEmpty) {
final targetIterableType = switch (targetType) {
InterfaceType(isDartCoreIterable: true) => targetType,
InterfaceType(:final allSupertypes) => allSupertypes.firstWhereOrNull(
(e) => e.isDartCoreIterable,
),
};
final objectType = targetIterableType?.typeArguments.firstOrNull;
final castedType = arguments.first.type;
if (castedType == null || objectType == null) {
return false;
}
final typeCast = TypeCast(source: objectType, target: castedType);
return typeCast.isUnnecessaryTypeCheck;
}
return false;
}| @override | ||
| Future<void> compute(ChangeBuilder builder) async { | ||
| final isExpressionNode = node.thisOrAncestorOfType<IsExpression>(); | ||
| if (isExpressionNode != null) { | ||
| final operatorOffset = isExpressionNode.isOperator.offset - 1; | ||
| _partToRemove = _removedPartRange(isExpressionNode, operatorOffset); | ||
| } | ||
|
|
||
| final whereTypeNode = node.thisOrAncestorOfType<MethodInvocation>(); | ||
| if (whereTypeNode != null && _partToRemove == null) { | ||
| final operatorOffset = | ||
| whereTypeNode.operator?.offset ?? whereTypeNode.offset; | ||
| _partToRemove = _removedPartRange(whereTypeNode, operatorOffset); | ||
| } | ||
|
|
||
| final partToRemove = _partToRemove; | ||
| if (partToRemove == null) return; | ||
|
|
||
| await builder.addDartFileEdit( | ||
| file, | ||
| (builder) => builder.addDeletion(partToRemove), | ||
| ); | ||
| } |
There was a problem hiding this comment.
If whereType is called implicitly on this (e.g., whereType<double>()), whereTypeNode.operator is null. In this case, operatorOffset defaults to whereTypeNode.offset, and _partToRemove covers the entire node. Deleting the entire node will result in a syntax error (e.g., final result = ;).
Instead, when there is no operator, the entire node should be replaced with 'this' to maintain syntactic and semantic correctness.
@override
Future<void> compute(ChangeBuilder builder) async {
final isExpressionNode = node.thisOrAncestorOfType<IsExpression>();
if (isExpressionNode != null) {
final operatorOffset = isExpressionNode.isOperator.offset - 1;
_partToRemove = _removedPartRange(isExpressionNode, operatorOffset);
}
final whereTypeNode = node.thisOrAncestorOfType<MethodInvocation>();
if (whereTypeNode != null && _partToRemove == null) {
final operator = whereTypeNode.operator;
if (operator != null) {
final operatorOffset = operator.offset;
_partToRemove = _removedPartRange(whereTypeNode, operatorOffset);
} else {
await builder.addDartFileEdit(
file,
(builder) => builder.addSimpleReplacement(
whereTypeNode.sourceRange,
'this',
),
);
return;
}
}
final partToRemove = _partToRemove;
if (partToRemove == null) return;
await builder.addDartFileEdit(
file,
(builder) => builder.addDeletion(partToRemove),
);
}| String? _findNearestAnalysisOptionsFilePath(String packageRootPath) { | ||
| final pathContext = _resourceProvider.pathContext; | ||
| String currentDirectoryPath = packageRootPath; | ||
|
|
||
| while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) { | ||
| final candidatePath = | ||
| pathContext.join(currentDirectoryPath, 'analysis_options.yaml'); | ||
| final candidateFile = _resourceProvider.getFile(candidatePath); | ||
|
|
||
| if (candidateFile.exists) { | ||
| return candidatePath; | ||
| } | ||
|
|
||
| final parentDir = pathContext.dirname(currentDirectoryPath); | ||
| currentDirectoryPath = parentDir; | ||
| } | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
The loop condition while (pathContext.dirname(currentDirectoryPath) != currentDirectoryPath) will terminate immediately if packageRootPath is the root directory (e.g., / or C:\), meaning it will never check for analysis_options.yaml at the root directory itself.
To make this search robust across all environments (including containers or environments where the project is at the root), we should check the current directory first before checking the loop termination condition.
String? _findNearestAnalysisOptionsFilePath(String packageRootPath) {
final pathContext = _resourceProvider.pathContext;
String currentDirectoryPath = packageRootPath;
while (true) {
final candidatePath =
pathContext.join(currentDirectoryPath, 'analysis_options.yaml');
final candidateFile = _resourceProvider.getFile(candidatePath);
if (candidateFile.exists) {
return candidatePath;
}
final parentDir = pathContext.dirname(currentDirectoryPath);
if (parentDir == currentDirectoryPath) {
break;
}
currentDirectoryPath = parentDir;
}
return null;
}
No description provided.