Skip to content

refactor: migrate function_lines_of_code#295

Closed
solid-illiaaihistov wants to merge 17 commits into
solid-software:masterfrom
solid-illiaaihistov:251-migrate-function_lines_of_code
Closed

refactor: migrate function_lines_of_code#295
solid-illiaaihistov wants to merge 17 commits into
solid-software:masterfrom
solid-illiaaihistov:251-migrate-function_lines_of_code

Conversation

@solid-illiaaihistov

@solid-illiaaihistov solid-illiaaihistov commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

Islam-Shaaban-Ibrahim and others added 17 commits April 15, 2026 12:53
* 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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +58 to +74
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;
  }

Comment on lines +45 to +73
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
  }

Comment on lines +38 to 60
@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),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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),
    );
  }

Comment on lines +61 to +79
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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;
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants