Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions lib/src/common/parameters/ignored_types_list_parameter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:analyzer/dart/element/type.dart';

import 'package:equatable/equatable.dart';
import 'package:solid_lints/src/utils/types_utils.dart';

/// A parameter model representing ignored types for linting.
/// It defines types that indicate when expressions, variables, or return values
/// should be ignored during analysis.
///
/// @docType String | List<String> | Map<String, Object?>
class IgnoredTypesListParameter extends Equatable {
/// The set of ignored type names.
final Set<String> ignoredTypes;

/// A common parameter key for analysis_options.yaml
static const String ignoredTypesKey = 'ignored_types';

/// Constructor for [IgnoredTypesListParameter] class.
const IgnoredTypesListParameter({
required this.ignoredTypes,
});

/// Empty [IgnoredTypesListParameter] model.
factory IgnoredTypesListParameter.empty() => const IgnoredTypesListParameter(
ignoredTypes: {},
);

/// Method for creating from json data.
factory IgnoredTypesListParameter.fromJson(Map<String, Object?> json) {
final raw = json[ignoredTypesKey];
final types = switch (raw) {
final Iterable<Object?> list => list.whereType<String>().toSet(),
final Map<Object?, Object?> map => map.keys.whereType<String>().toSet(),
final String str => {str},
_ => const <String>{},
};

return IgnoredTypesListParameter(ignoredTypes: types);
}

/// Returns whether the target type should be ignored during analysis.
bool shouldIgnore(DartType? type) {
if (type == null || ignoredTypes.isEmpty) return false;
return type.hasIgnoredType(ignoredTypes: ignoredTypes);
}

/// Returns `true` if any of the target [types] should be ignored.
bool shouldIgnoreAny(Iterable<DartType?> types) => types.any(shouldIgnore);

@override
List<Object?> get props => [ignoredTypes];
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart';

/// A data model class that represents the "avoid late keyword" input
/// parameters.
class AvoidLateKeywordParameters {
Expand All @@ -24,20 +26,18 @@ class AvoidLateKeywordParameters {
/// late ColorTween tween; // OK
/// late int colorValue; // LINT
/// ```
final Iterable<String> ignoredTypes;
final IgnoredTypesListParameter ignoredTypes;

/// Constructor for [AvoidLateKeywordParameters] model
const AvoidLateKeywordParameters({
this.allowInitialized = false,
this.ignoredTypes = const [],
this.ignoredTypes = const IgnoredTypesListParameter(ignoredTypes: {}),
});

/// Method for creating from json data
factory AvoidLateKeywordParameters.fromJson(Map<String, Object?> json) =>
AvoidLateKeywordParameters(
allowInitialized: json['allow_initialized'] as bool? ?? false,
ignoredTypes: List<String>.from(
json['ignored_types'] as Iterable? ?? [],
),
ignoredTypes: IgnoredTypesListParameter.fromJson(json),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/src/lints/avoid_late_keyword/avoid_late_keyword_rule.dart';
import 'package:solid_lints/src/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart';
import 'package:solid_lints/src/utils/types_utils.dart';

/// Visitor for [AvoidLateKeywordRule].
class AvoidLateKeywordVisitor extends SimpleAstVisitor<void> {
Expand All @@ -26,8 +25,7 @@ class AvoidLateKeywordVisitor extends SimpleAstVisitor<void> {
!(_parameters.allowInitialized && node.initializer != null);

bool _hasIgnoredType(VariableDeclaration node) =>
node.declaredFragment?.element.type.hasIgnoredType(
ignoredTypes: _parameters.ignoredTypes.toSet(),
) ??
false;
_parameters.ignoredTypes.shouldIgnore(
node.declaredFragment?.element.type,
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart';

/// A data model class that represents the "avoid non null assertion" input
/// parameters.
class AvoidNonNullAssertionParameters {
Expand All @@ -18,7 +20,7 @@ class AvoidNonNullAssertionParameters {
/// Map<String, String> map;
/// map['key']!; // OK
/// ```
final Set<String> ignoredTypes;
final IgnoredTypesListParameter ignoredTypes;

/// Constructor for [AvoidNonNullAssertionParameters] model
const AvoidNonNullAssertionParameters({
Expand All @@ -27,23 +29,14 @@ class AvoidNonNullAssertionParameters {

/// Empty [AvoidNonNullAssertionParameters] model, ignores nothing.
factory AvoidNonNullAssertionParameters.empty() =>
const AvoidNonNullAssertionParameters(
ignoredTypes: {},
AvoidNonNullAssertionParameters(
ignoredTypes: IgnoredTypesListParameter.empty(),
);

/// Method for creating from json data
factory AvoidNonNullAssertionParameters.fromJson(Map<String, Object?> json) {
final raw = json['ignored_types'];
final excludeList = switch (raw) {
final Iterable<Object?> rawList => rawList.whereType<String>().toSet(),
final Map<Object?, Object?> rawMap =>
rawMap.keys.whereType<String>().toSet(),
final String rawString => {rawString},
_ => const <String>{},
};

return AvoidNonNullAssertionParameters(
ignoredTypes: excludeList,
);
}
factory AvoidNonNullAssertionParameters.fromJson(
Map<String, Object?> json,
) => AvoidNonNullAssertionParameters(
ignoredTypes: IgnoredTypesListParameter.fromJson(json),
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:solid_lints/src/lints/avoid_non_null_assertion/avoid_non_null_assertion_rule.dart';
import 'package:solid_lints/src/lints/avoid_non_null_assertion/models/avoid_non_null_assertion_parameters.dart';
import 'package:solid_lints/src/utils/types_utils.dart';

/// visitor for [AvoidNonNullAssertionRule]
class AvoidNonNullAssertionVisitor extends SimpleAstVisitor<void> {
Expand All @@ -27,21 +25,11 @@ class AvoidNonNullAssertionVisitor extends SimpleAstVisitor<void> {
if (operand is IndexExpression) {
final type = operand.target?.staticType;

if (_hasIgnoredType(type)) {
if (_parameters.ignoredTypes.shouldIgnore(type)) {
return;
}
}

rule.reportAtNode(node);
}

bool _hasIgnoredType(DartType? type) {
if (type == null) {
return false;
}

return type.hasIgnoredType(
ignoredTypes: _parameters.ignoredTypes,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// solid_lints:
/// diagnostics:
/// avoid_returning_widgets:
/// ignored_types:
/// - MultiProvider
/// - InheritedProvider
/// - InheritedTheme
/// exclude:
/// - class_name: MyWidget
/// method_name: buildCustomButton
Expand Down Expand Up @@ -57,6 +61,13 @@ import 'package:solid_lints/src/models/solid_lint_rule.dart';
/// return const SizedBox();
/// }
/// }
///
/// // Allowed if MultiProvider / InheritedTheme is in ignored_types:
/// MultiProvider buildProviders(Widget child) => MultiProvider(
/// providers: [],
/// child: child,
/// );
/// InputDecorationTheme get inputTheme => const InputDecorationTheme();
/// ```
class AvoidReturningWidgetsRule
extends SolidLintRule<AvoidReturningWidgetsParameters> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';
import 'package:solid_lints/src/common/parameters/ignored_types_list_parameter.dart';
import 'package:solid_lints/src/utils/node_utils.dart';

/// A data model class that represents the "avoid returning widgets" input
/// parameters.
class AvoidReturningWidgetsParameters {
/// A list of methods that should be excluded from the lint.
final ExcludedIdentifiersListParameter exclude;

/// Types that would be ignored by avoid-returning-widgets rule.
///
/// Example:
///
/// ```yaml
/// solid_lints:
/// diagnostics:
/// avoid_returning_widgets:
/// ignored_types:
/// - MultiProvider
/// - InheritedTheme
/// ```
///
/// ```dart
/// MultiProvider providers(Widget child) => MultiProvider(...); // OK
/// ```
final IgnoredTypesListParameter ignoredTypes;

/// Constructor for [AvoidReturningWidgetsParameters] model
AvoidReturningWidgetsParameters({
const AvoidReturningWidgetsParameters({
required this.exclude,
required this.ignoredTypes,
});

/// Empty [AvoidReturningWidgetsParameters] model, excludes nothing.
factory AvoidReturningWidgetsParameters.empty() {
return AvoidReturningWidgetsParameters(
exclude: ExcludedIdentifiersListParameter(exclude: []),
);
}
factory AvoidReturningWidgetsParameters.empty() =>
AvoidReturningWidgetsParameters(
exclude: ExcludedIdentifiersListParameter(exclude: []),
ignoredTypes: IgnoredTypesListParameter.empty(),
);

/// Method for creating from json data
factory AvoidReturningWidgetsParameters.fromJson(Map<String, dynamic> json) {
return AvoidReturningWidgetsParameters(
exclude: ExcludedIdentifiersListParameter.defaultFromJson(json),
);
factory AvoidReturningWidgetsParameters.fromJson(
Map<String, Object?> json,
) => AvoidReturningWidgetsParameters(
exclude: ExcludedIdentifiersListParameter.defaultFromJson(json),
ignoredTypes: IgnoredTypesListParameter.fromJson(json),
);

/// Returns `true` if the given [node] should be ignored by the lint rule.
bool shouldIgnore(Declaration node) {
return ignoredTypes.shouldIgnoreAny([
node.returnType,
node.singleReturnExpression?.staticType,
]) ||
exclude.shouldIgnore(node);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,33 +36,18 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor<void> {
}

void _visitDeclaration(Declaration node) {
if (node is! FunctionDeclaration && node is! MethodDeclaration) {
return;
}

if (node is MethodDeclaration &&
(!node.isComplete ||
node.body is EmptyFunctionBody ||
(node.isGetter && _isStateWidgetCastingGetter(node)))) {
return;
}

final returnType = switch (node) {
MethodDeclaration(:final declaredFragment?) =>
declaredFragment.element.returnType,
FunctionDeclaration(:final declaredFragment?) =>
declaredFragment.element.returnType,
_ => null,
};
if (returnType == null) return;

final isWidgetReturned = isWidgetType(returnType);
if (!isWidgetReturned) return;

final isIgnored = _parameters.exclude.shouldIgnore(node);
if (isIgnored) return;

if (_isOverridden(node)) return;
if (!isWidgetOrSubclass(node.returnType) ||
_parameters.shouldIgnore(node) ||
_isOverridden(node)) {
return;
}

_rule.reportAtNode(node);
}
Expand All @@ -89,12 +74,8 @@ class AvoidReturningWidgetsVisitor extends RecursiveAstVisitor<void> {
}

bool _isOverridden(Declaration node) {
if (node is MethodDeclaration &&
node.metadata.any((m) => m.name.name == 'override')) {
return true;
}

return switch (node) {
MethodDeclaration(:final metadata) when isOverride(metadata) => true,
Declaration(
declaredFragment: Fragment(
element: Element(
Expand Down
33 changes: 28 additions & 5 deletions lib/src/utils/node_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,11 @@ extension ExpressionNullableExtension on Expression? {
bool get isThisOrSuper => this is ThisExpression || this is SuperExpression;
}

/// Extension on [MethodDeclaration] to provide AST helper getters.
extension MethodDeclarationExtension on MethodDeclaration {
/// Returns the single return expression of a method, or null if the
/// method body has multiple statements or no return expression.
Expression? get singleReturnExpression => switch (body) {
/// Extension on [FunctionBody] to provide AST helper getters.
extension FunctionBodyExtension on FunctionBody {
/// Returns the single return expression of a function body, or null if the
/// body has multiple statements or no return expression.
Expression? get singleReturnExpression => switch (this) {
ExpressionFunctionBody(:final expression) => expression,
BlockFunctionBody(
block: Block(statements: [ReturnStatement(:final expression?)]),
Expand All @@ -340,3 +340,26 @@ extension MethodDeclarationExtension on MethodDeclaration {
_ => null,
};
}

/// Extension on [Declaration] to provide AST helper getters.
extension DeclarationExtension on Declaration {
/// Returns the single return expression of a declaration (method or
/// function), or null if the body has multiple statements or no return
/// expression.
Expression? get singleReturnExpression => switch (this) {
MethodDeclaration(:final body) => body.singleReturnExpression,
FunctionDeclaration(:final functionExpression) =>
functionExpression.body.singleReturnExpression,
_ => null,
};

/// Returns the declared return type of a declaration (method or
/// function), or null if none.
DartType? get returnType => switch (this) {
MethodDeclaration(:final declaredFragment?) =>
declaredFragment.element.returnType,
FunctionDeclaration(:final declaredFragment?) =>
declaredFragment.element.returnType,
_ => null,
};
}
Loading