Skip to content
6 changes: 4 additions & 2 deletions lib/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,16 @@ solid_lints:
allow_initialized: true
ignored_types:
- AnimationController

avoid_non_null_assertion: true
avoid_returning_widgets: true
avoid_unnecessary_return_variable: true
avoid_unnecessary_setstate: true
avoid_unnecessary_type_assertions: true
avoid_unrelated_type_assertions: true
avoid_unused_parameters: true
avoid_unused_parameters:
exclude_annotation:
- freezed
- unfreezed
avoid_debug_print_in_release: true
avoid_final_with_getter: true

Expand Down
58 changes: 58 additions & 0 deletions lib/src/common/parameters/excluded_annotations_list_parameter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'package:analyzer/dart/ast/ast.dart';

/// A parameter model representing excluded annotations for linting.
/// It defines class-level annotations that indicate when class members
/// should be ignored during analysis.
class ExcludedAnnotationsListParameter {
/// The set of excluded annotation names.
final Set<String> excludedAnnotations;

/// A common parameter key for analysis_options.yaml
static const String excludeAnnotationKey = 'exclude_annotation';

/// Constructor for [ExcludedAnnotationsListParameter] class
ExcludedAnnotationsListParameter({
required this.excludedAnnotations,
});

/// Method for creating from json data
factory ExcludedAnnotationsListParameter.fromJson(Map<String, dynamic> json) {
final raw = json[excludeAnnotationKey];
if (raw is Iterable) {
return ExcludedAnnotationsListParameter(
excludedAnnotations: Set<String>.from(raw.whereType<String>()),
);
} else if (raw is String) {
return ExcludedAnnotationsListParameter(
excludedAnnotations: {raw},
);
}

return ExcludedAnnotationsListParameter(
excludedAnnotations: {},
);
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.

/// Returns whether the target node should be ignored during analysis because
/// its enclosing declaration is annotated with one of the excluded
/// annotations.
bool shouldIgnore(Declaration node) {
if (excludedAnnotations.isEmpty) return false;

AstNode? current = node;
while (current != null) {
if (current is Declaration) {
final hasAnnotation = current.metadata.any((annotation) {
final name = annotation.name.name;
final simpleName = name.split('.').last;
return excludedAnnotations.contains(name) ||
excludedAnnotations.contains(simpleName);
});
Comment thread
solid-illiaaihistov marked this conversation as resolved.
if (hasAnnotation) return true;
}
current = current.parent;
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.

return false;
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@ class ExcludedEntitiesListParameter {
/// Method for creating from json data
factory ExcludedEntitiesListParameter.fromJson(Map<String, dynamic> json) {
final raw = json['exclude_entity'];
if (raw is List) {
if (raw is Iterable) {
return ExcludedEntitiesListParameter(
excludedEntityNames: Set<String>.from(raw),
excludedEntityNames: Set<String>.from(raw.whereType<String>()),
);
} else if (raw is String) {
return ExcludedEntitiesListParameter(
excludedEntityNames: {raw},
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@ class ExcludedIdentifiersListParameter {
factory ExcludedIdentifiersListParameter.defaultFromJson(
Map<String, dynamic> json,
) {
final excludeList =
json[ExcludedIdentifiersListParameter.excludeParameterName]
as Iterable? ??
[];
final raw = json[ExcludedIdentifiersListParameter.excludeParameterName];

final excludeList = switch (raw) {
Iterable() => raw,
Map() || String() => [raw],
_ => const [],
};

return ExcludedIdentifiersListParameter.fromJson(
excludeList: excludeList,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// Parameters whose names consist only of underscores are also ignored.
/// Overridden methods and methods used as tear-offs are skipped.
///
/// ### Parameters
///
/// #### exclude_annotation
/// A list of class annotations (such as `freezed` or `unfreezed`) whose
/// constructor parameters should be ignored by this rule (useful for code
/// generation).
///
/// {@template solid_lints.avoid_unused_parameters.example}
/// ### Example
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:solid_lints/src/common/parameters/excluded_annotations_list_parameter.dart';
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';

/// A data model class that represents the `avoid_unused_parameters` input
Expand All @@ -6,22 +7,30 @@ class AvoidUnusedParametersParameters {
/// A list of methods that should be excluded from the lint.
final ExcludedIdentifiersListParameter exclude;

/// A list of annotations that should be ignored during class check.
final ExcludedAnnotationsListParameter excludeAnnotation;

/// Constructor for [AvoidUnusedParametersParameters] model.
AvoidUnusedParametersParameters({
required this.exclude,
required this.excludeAnnotation,
});

/// Empty [AvoidUnusedParametersParameters] model, excludes nothing.
factory AvoidUnusedParametersParameters.empty() {
return AvoidUnusedParametersParameters(
exclude: ExcludedIdentifiersListParameter(exclude: []),
excludeAnnotation: ExcludedAnnotationsListParameter(
excludedAnnotations: {},
),
);
}

/// Method for creating from json data.
factory AvoidUnusedParametersParameters.fromJson(Map<String, dynamic> json) {
return AvoidUnusedParametersParameters(
exclude: ExcludedIdentifiersListParameter.defaultFromJson(json),
excludeAnnotation: ExcludedAnnotationsListParameter.fromJson(json),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
final parent = node.parent;
final parameters = node.parameters;

if (parent is ClassDeclaration && parent.abstractKeyword != null ||
if ((parent is ClassDeclaration && parent.abstractKeyword != null) ||
node.externalKeyword != null ||
node.redirectedConstructor != null ||
_hasExcludedAnnotationClass(node) ||
parameters.parameters.isEmpty) {
return;
}
Expand All @@ -71,7 +73,7 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {
final parent = node.parent;
final parameters = node.parameters;

if (parent is ClassDeclaration && parent.abstractKeyword != null ||
if ((parent is ClassDeclaration && parent.abstractKeyword != null) ||
node.isAbstract ||
node.externalKeyword != null ||
(parameters == null || parameters.parameters.isEmpty)) {
Expand Down Expand Up @@ -113,6 +115,9 @@ class AvoidUnusedParametersVisitor extends RecursiveAstVisitor<void> {

bool _isExcluded(Declaration node) => _parameters.exclude.shouldIgnore(node);

bool _hasExcludedAnnotationClass(ConstructorDeclaration node) =>
_parameters.excludeAnnotation.shouldIgnore(node);

Iterable<FormalParameter> _filterOutUnderscoresAndNamed(
AstNode body,
Iterable<FormalParameter> parameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Placeholder extends StatelessWidget {
'SimpleClassName',
'exclude',
],
'exclude_annotation': ['freezed'],
},
);

Expand Down Expand Up @@ -345,6 +346,69 @@ class SimpleClassName {
return;
}
}
''');
}

Future<void>
test_does_not_report_on_redirecting_factory_constructors() async {
await assertNoDiagnostics(r'''
class RedirectingClass {
const factory RedirectingClass({required int parameter}) = _RedirectingClass;
}

class _RedirectingClass implements RedirectingClass {
final int parameter;
const _RedirectingClass({required this.parameter});
}
''');
}

Future<void> test_does_not_report_on_freezed_classes() async {
await assertNoDiagnostics(r'''
const freezed = Object();

@freezed
class Test {
const Test(int unusedParameter);
}
''');
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.

Future<void>
test_does_not_report_on_excluded_declaration_and_annotation_single_string() async {
final FakeAnalysisOptionsLoader fakeAnalysisOptionsLoader =
FakeAnalysisOptionsLoader(
ruleOptions: {
'exclude': 'excludeMethod',
'exclude_annotation': 'freezed',
},
);

rule = AvoidUnusedParametersRule(
analysisOptionsLoader: fakeAnalysisOptionsLoader,
);

await assertNoDiagnostics(r'''
const freezed = Object();

@freezed
class Test {
const Test(int unusedParameter);
}

class Meta {
const Meta();
}
const meta = Meta();

@meta.freezed
class TestWithPrefix {
const TestWithPrefix(int unusedParameter);
}

void excludeMethod(String s) {
return;
}
''');
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.
87 changes: 87 additions & 0 deletions test/src/common/parameters/parameters_parsing_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import 'package:solid_lints/src/common/parameters/excluded_annotations_list_parameter.dart';
import 'package:solid_lints/src/common/parameters/excluded_entities_list_parameter.dart';
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';
import 'package:test/test.dart';

void main() {
group('ExcludedAnnotationsListParameter', () {
test('parses list of strings', () {
final param = ExcludedAnnotationsListParameter.fromJson({
'exclude_annotation': ['MyAnnotation1', 'MyAnnotation2'],
});
expect(param.excludedAnnotations, containsAll(['MyAnnotation1', 'MyAnnotation2']));
});

test('parses single string', () {
final param = ExcludedAnnotationsListParameter.fromJson({
'exclude_annotation': 'MyAnnotation',
});
expect(param.excludedAnnotations, contains('MyAnnotation'));
expect(param.excludedAnnotations.length, 1);
});

test('parses empty or invalid input', () {
final param = ExcludedAnnotationsListParameter.fromJson({});
expect(param.excludedAnnotations, isEmpty);
});
});

group('ExcludedEntitiesListParameter', () {
test('parses list of strings', () {
final param = ExcludedEntitiesListParameter.fromJson({
'exclude_entity': ['mixin', 'enum'],
});
expect(param.excludedEntityNames, containsAll(['mixin', 'enum']));
});

test('parses single string', () {
final param = ExcludedEntitiesListParameter.fromJson({
'exclude_entity': 'mixin',
});
expect(param.excludedEntityNames, contains('mixin'));
expect(param.excludedEntityNames.length, 1);
});

test('parses empty or invalid input', () {
final param = ExcludedEntitiesListParameter.fromJson({});
expect(param.excludedEntityNames, isEmpty);
});
});

group('ExcludedIdentifiersListParameter', () {
test('parses list of strings and maps', () {
final param = ExcludedIdentifiersListParameter.defaultFromJson({
'exclude': [
'my_function',
{'class_name': 'MyClass', 'method_name': 'my_method'},
],
});
expect(param.exclude.length, 2);
expect(param.exclude[0].declarationName, 'my_function');
expect(param.exclude[1].className, 'MyClass');
expect(param.exclude[1].methodName, 'my_method');
});

test('parses single string', () {
final param = ExcludedIdentifiersListParameter.defaultFromJson({
'exclude': 'my_function',
});
expect(param.exclude.length, 1);
expect(param.exclude[0].declarationName, 'my_function');
});

test('parses single map', () {
final param = ExcludedIdentifiersListParameter.defaultFromJson({
'exclude': {'class_name': 'MyClass', 'method_name': 'my_method'},
});
expect(param.exclude.length, 1);
expect(param.exclude[0].className, 'MyClass');
expect(param.exclude[0].methodName, 'my_method');
});

test('parses empty or invalid input', () {
final param = ExcludedIdentifiersListParameter.defaultFromJson({});
expect(param.exclude, isEmpty);
});
});
}
Loading