Skip to content

Commit

Permalink
Create ignored_entities_model; use it for avoid_unused_parameters & a…
Browse files Browse the repository at this point in the history
…void_returning_widgets
  • Loading branch information
sufftea committed Apr 30, 2024
1 parent e55da4c commit 719a626
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:analyzer/error/listener.dart';
import 'package:collection/collection.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_parameters.dart';
import 'package:solid_lints/src/models/ignored_entities_model/ignored_entities_model.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';
import 'package:solid_lints/src/utils/types_utils.dart';
Expand Down Expand Up @@ -50,8 +49,7 @@ import 'package:solid_lints/src/utils/types_utils.dart';
/// }
/// }
/// ```
class AvoidReturningWidgetsRule
extends SolidLintRule<AvoidReturningWidgetsParameters> {
class AvoidReturningWidgetsRule extends SolidLintRule<IgnoredEntitiesModel> {
/// The [LintCode] of this lint rule that represents
/// the error whether we return a widget.
static const lintName = 'avoid_returning_widgets';
Expand All @@ -64,7 +62,7 @@ class AvoidReturningWidgetsRule
final rule = RuleConfig(
configs: configs,
name: lintName,
paramsParser: AvoidReturningWidgetsParameters.fromJson,
paramsParser: IgnoredEntitiesModel.fromJson,
problemMessage: (_) =>
'Returning a widget from a function is considered an anti-pattern. '
'Unless you are overriding an existing method, '
Expand Down Expand Up @@ -96,7 +94,7 @@ class AvoidReturningWidgetsRule

final isWidgetReturned = hasWidgetType(returnType);

final isIgnored = _shouldIgnore(node);
final isIgnored = config.parameters.matchMethod(node);

final isOverriden = node.declaredElement?.hasOverride ?? false;

Expand All @@ -105,25 +103,4 @@ class AvoidReturningWidgetsRule
}
});
}

bool _shouldIgnore(Declaration node) {
final methodName = node.declaredElement?.name;

final excludedItem = config.parameters.exclude
.firstWhereOrNull((e) => e.methodName == methodName);

if (excludedItem == null) return false;

final className = excludedItem.className;

if (className == null || node is! MethodDeclaration) {
return true;
} else {
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>();

if (classDeclaration == null) return false;

return classDeclaration.name.toString() == className;
}
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/src/lints/avoid_unused_parameters/visitors/avoid_unused_parameters_visitor.dart';
import 'package:solid_lints/src/models/ignored_entities_model/ignored_entities_model.dart';
import 'package:solid_lints/src/models/rule_config.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';

Expand Down Expand Up @@ -64,7 +65,7 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// };
///
/// ```
class AvoidUnusedParametersRule extends SolidLintRule {
class AvoidUnusedParametersRule extends SolidLintRule<IgnoredEntitiesModel> {
/// The [LintCode] of this lint rule that represents
/// the error whether we use bad formatted double literals.
static const String lintName = 'avoid_unused_parameters';
Expand All @@ -79,6 +80,7 @@ class AvoidUnusedParametersRule extends SolidLintRule {
final rule = RuleConfig(
configs: configs,
name: lintName,
paramsParser: IgnoredEntitiesModel.fromJson,
problemMessage: (_) => 'Parameter is unused.',
);

Expand All @@ -92,7 +94,7 @@ class AvoidUnusedParametersRule extends SolidLintRule {
CustomLintContext context,
) {
context.registry.addCompilationUnit((node) {
final visitor = AvoidUnusedParametersVisitor();
final visitor = AvoidUnusedParametersVisitor(config.parameters);
node.accept(visitor);

for (final element in visitor.unusedParameters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:collection/collection.dart';
import 'package:solid_lints/src/models/ignored_entities_model/ignored_entities_model.dart';
import 'package:solid_lints/src/utils/node_utils.dart';
import 'package:solid_lints/src/utils/parameter_utils.dart';

/// AST Visitor which finds all is expressions and checks if they are
/// unrelated (result always false)
class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
/// AvoidUnusedParametersVisitor constructor
AvoidUnusedParametersVisitor(this.ignoredEntities);

/// Entities that should be ignored
final IgnoredEntitiesModel ignoredEntities;

final _unusedParameters = <FormalParameter>[];

/// List of unused parameters
Expand All @@ -48,6 +55,13 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
parameters.parameters.isEmpty) {
return;
}

if (node.thisOrAncestorOfType<ClassDeclaration>() case final classNode?) {
if (ignoredEntities.matchClass(classNode)) {
return;
}
}

final unused = _getUnusedParameters(
node.body,
parameters.parameters,
Expand All @@ -73,6 +87,16 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
return;
}

if (ignoredEntities.matchMethod(node)) {
return;
}

if (node.thisOrAncestorOfType<ClassDeclaration>() case final classNode?) {
if (ignoredEntities.matchClass(classNode)) {
return;
}
}

final isTearOff = _usedAsTearOff(node);

if (!isOverride(node.metadata) && !isTearOff) {
Expand All @@ -93,6 +117,12 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
return;
}

if (node.thisOrAncestorOfType<FunctionDeclaration>() case final funcNode?) {
if (ignoredEntities.matchMethod(funcNode)) {
return;
}
}

_unusedParameters.addAll(
_filterOutUnderscoresAndNamed(
node.body,
Expand Down
73 changes: 73 additions & 0 deletions lib/src/models/ignored_entities_model/ignored_entities_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:solid_lints/src/models/ignored_entities_model/ignored_entity.dart';

/// Manages a list of entities (functions/methods/classes) that should be
/// excluded from a lint rule.
///
/// Example config:
/// ```yaml
/// custom_lint:
/// rules:
/// - <rule_name>:
/// exclude:
/// # excludes a matching method in a matching class
/// - method_name: excludeMethod
/// class_name: ExcludeClass
/// # excludes a matching method anywhere
/// - method_name: excludeFunction
/// # excludes all methods within a matching class
/// - class_name: ExcludeEntireClass
/// ```
class IgnoredEntitiesModel {
IgnoredEntitiesModel._({required this.entities});

///
factory IgnoredEntitiesModel.fromJson(Map<dynamic, dynamic> json) {
final entities = <IgnoredEntity>[];
final excludeList = json['exclude'] as Iterable? ?? [];
for (final item in excludeList) {
if (item is Map) {
entities.add(IgnoredEntity.fromJson(item));
}
}
return IgnoredEntitiesModel._(entities: entities);
}

/// The entities to be ignored
final List<IgnoredEntity> entities;

/// Checks if the entire class should be ignored.
/// Doesn't match if the config specifies a specific function within the class
bool matchClass(ClassDeclaration node) {
final className = node.name.toString();

return entities.any((element) {
return element.functionName == null && element.className == className;
});
}

/// Checks if the given method/function should be ignored.
bool matchMethod(Declaration node) {
final methodName = node.declaredElement?.name;

return entities.any((entity) {
if (entity.functionName != methodName) {
return false;
}

if (entity.className == null) {
return true;
}

final matchingClass = node.thisOrAncestorMatching((node) {
if (node case final ClassDeclaration classNode) {
return classNode.name.toString() == entity.className;
}

return false;
});

return matchingClass != null;
});
}
}
25 changes: 25 additions & 0 deletions lib/src/models/ignored_entities_model/ignored_entity.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// An entity (method/function/class) to be excluded from lint
class IgnoredEntity {
IgnoredEntity._({
this.className,
this.functionName,
});

///
factory IgnoredEntity.fromJson(Map<dynamic, dynamic> json) {
return IgnoredEntity._(
className: json['class_name'] as String?,
functionName: json['method_name'] as String?,
);
}

/// Class name
final String? className;
/// Function name
final String? functionName;

@override
String toString() {
return "$className: $functionName";
}
}
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ analyzer:
- custom_lint

custom_lint:
debug: true,
rules:
- cyclomatic_complexity:
max_complexity: 4
Expand Down
12 changes: 12 additions & 0 deletions lint_test/avoid_unused_parameters_test/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
analyzer:
plugins:
- ../custom_lint

custom_lint:
rules:
- avoid_unused_parameters:
exclude:
- method_name: excludeMethod
class_name: ExcludeClass
- method_name: excludeFunction
- class_name: ExcludeEntireClass
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,13 @@ class UsingConstructorParameterInInitializer {
print(_value);
}
}

class ExcludeEntireClass {
void foo(int a) {}
}

void excludeFunction(int a, int b) {}

class ExcludeClass {
void excludeMethod(int a, int b) {}
}

0 comments on commit 719a626

Please sign in to comment.