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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,23 @@ dev_dependencies:

Enable the plugin and include `solid_lints` in your project's top-level `analysis_options.yaml`:

### Option 1: Using the `plugins` block

```yaml
include: package:solid_lints/analysis_options.yaml

plugins:
solid_lints:
```

### Option 2: Using the top-level `solid_lints` block

```yaml
include: package:solid_lints/analysis_options.yaml

solid_lints:
```

Also, you can use a specialized rule set designed for Dart tests.
Add an `analysis_options.yaml` file under the `test/` directory, and include the ruleset:

Expand All @@ -43,9 +53,25 @@ dart analyze;

# Configuration

You can customize individual rule settings in your `analysis_options.yaml` under the `solid_lints` configuration block:
You can customize individual rule settings in your `analysis_options.yaml`.

### Option 1: Inside the `plugins` block (Recommended)

```yaml
plugins:
solid_lints:
diagnostics:
cyclomatic_complexity:
max_complexity: 10
avoid_non_null_assertion: true
```

### Option 2: Separate top-level `solid_lints` block

```yaml
plugins:
solid_lints:

solid_lints:
diagnostics:
cyclomatic_complexity:
Expand Down
5 changes: 5 additions & 0 deletions lib/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ solid_lints:
- IMap
- BuiltMap
avoid_returning_widgets: true
avoid_similar_names: true
avoid_unnecessary_return_variable: true
avoid_unnecessary_setstate: true
avoid_unnecessary_type_assertions: true
Expand All @@ -67,6 +68,10 @@ solid_lints:
max_complexity: 10

double_literal_format: true
feature_envy:
atfd_threshold: 4
laa_threshold: 0.33
fdp_threshold: 2

function_lines_of_code:
max_lines: 200
Expand Down
2 changes: 2 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param
import 'package:solid_lints/src/lints/avoid_using_api/avoid_using_api_rule.dart';
import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexity_rule.dart';
import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart';
import 'package:solid_lints/src/lints/feature_envy/feature_envy_rule.dart';
import 'package:solid_lints/src/lints/function_lines_of_code/function_lines_of_code_rule.dart';
import 'package:solid_lints/src/lints/member_ordering/member_ordering_rule.dart';
import 'package:solid_lints/src/lints/named_parameters_ordering/named_parameters_ordering_rule.dart';
Expand Down Expand Up @@ -74,6 +75,7 @@ class SolidLintsPlugin extends Plugin {
AvoidUsingApiRule(analysisOptionsLoader: analysisLoader),
CyclomaticComplexityRule(analysisOptionsLoader: analysisLoader),
DoubleLiteralFormatRule(),
FeatureEnvyRule(analysisOptionsLoader: analysisLoader),
FunctionLinesOfCodeRule(analysisOptionsLoader: analysisLoader),
MemberOrderingRule(analysisOptionsLoader: analysisLoader),
NamedParametersOrderingRule(analysisOptionsLoader: analysisLoader),
Expand Down
89 changes: 89 additions & 0 deletions lib/src/lints/feature_envy/feature_envy_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/error/error.dart';
import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_parameters.dart';
import 'package:solid_lints/src/lints/feature_envy/visitors/feature_envy_visitor.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';

/// Warns if a method accesses fields or methods from a different class
/// more often than from its own class (feature envy).
///
/// ### Example
/// BAD:
/// ```dart
/// class A {
/// int field;
/// A(this.field);
/// }
///
/// class B {
/// int method(A a) => a.field * a.field; // LINT
/// }
/// ```
///
/// GOOD:
/// ```dart
/// class A {
/// int field;
/// A(this.field);
///
/// int method() => field * field;
/// }
///
/// class B {
/// int method(A a) => a.method();
/// }
/// ```
///
/// ### Detection Algorithm
/// The rule detects feature envy using three metrics:
/// - **ATFD** (Access to Foreign Data): Accesses to members of a single
/// external class. Triggers if ATFD >= threshold (default 4).
/// - **LAA** (Locality of Attribute Access): The ratio of internal accesses
/// to total accesses. Triggers if LAA < threshold (default 0.33).
/// - **FDP** (Foreign Data Providers): The number of unique external classes
/// accessed. Triggers if FDP <= threshold (default 2).
///
/// Accesses to other instances of the same class, non-project classes,
/// closures, nested functions, and data classes are ignored.
class FeatureEnvyRule extends SolidLintRule<FeatureEnvyParameters> {
/// Name of the lint.
static const lintName = 'feature_envy';

static const _code = LintCode(
lintName,
"Avoid accessing members of `{0}` in `{1}` more often than own members.",
correctionMessage:
"Consider moving the related logic into the `{0}` class.",
);

@override
DiagnosticCode get diagnosticCode => _code;

/// Creates a new instance of [FeatureEnvyRule].
FeatureEnvyRule({
required super.analysisOptionsLoader,
}) : super.withParameters(
name: lintName,
description:
'Warns if a method accesses members of another class more '
'often than its own (feature envy).',
parametersParser: FeatureEnvyParameters.fromJson,
);

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
super.registerNodeProcessors(registry, context);

final parameters =
getParametersForContext(context) ?? FeatureEnvyParameters.empty();

registry.addMethodDeclaration(
this,
FeatureEnvyVisitor(this, parameters),
);
}
}
52 changes: 52 additions & 0 deletions lib/src/lints/feature_envy/models/feature_envy_metrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:collection/collection.dart';
import 'package:solid_lints/src/lints/feature_envy/models/feature_envy_parameters.dart';
import 'package:solid_lints/src/utils/iterable_utils.dart';

/// Calculated Feature Envy metrics for a method.
class FeatureEnvyMetrics {
/// Locality of Attribute Access (LAA) metric.
final double laa;

/// Foreign Data Providers (FDP) metric.
final int fdp;

/// Access to Foreign Data (ATFD) metric.
final int atfd;

/// The class element that is accessed the most externally.
final InterfaceElement? maxEnvyElement;

const FeatureEnvyMetrics._({
required this.laa,
required this.fdp,
required this.atfd,
required this.maxEnvyElement,
});

/// Checks if these metrics exceed the thresholds defined in [parameters],
/// indicating a feature envy code smell.
bool exceedsThresholds(FeatureEnvyParameters parameters) =>
atfd >= parameters.atfdThreshold &&
laa < parameters.laaThreshold &&
fdp <= parameters.fdpThreshold;

/// Calculates metrics based on collected accesses.
factory FeatureEnvyMetrics.calculate({
required int internalAccesses,
required Map<InterfaceElement, int> externalAccessCounts,
}) {
final totalAccesses = internalAccesses + externalAccessCounts.values.sum;
final maxEntry = externalAccessCounts.entries.multiSortedBy([
(e) => -e.value,
(e) => e.key.name ?? '',
]).firstOrNull;

return FeatureEnvyMetrics._(
laa: totalAccesses == 0 ? 1.0 : internalAccesses / totalAccesses,
fdp: externalAccessCounts.length,
atfd: maxEntry?.value ?? 0,
maxEnvyElement: maxEntry?.key,
);
}
}
51 changes: 51 additions & 0 deletions lib/src/lints/feature_envy/models/feature_envy_parameters.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';

/// Configuration parameters for the feature_envy rule.
class FeatureEnvyParameters {
/// A list of methods that should be excluded from the lint.
final ExcludedIdentifiersListParameter exclude;

/// Access to Foreign Data (ATFD) threshold.
/// Triggered if ATFD >= atfdThreshold.
final int atfdThreshold;

/// Locality of Attribute Access (LAA) threshold.
/// Triggered if LAA < laaThreshold.
final double laaThreshold;

/// Foreign Data Providers (FDP) threshold.
/// Triggered only if FDP <= fdpThreshold.
final int fdpThreshold;

/// Default Access to Foreign Data (ATFD) threshold.
static const _defaultAtfdThreshold = 4;

/// Default Locality of Attribute Access (LAA) threshold.
static const _defaultLaaThreshold = 0.33;

/// Default Foreign Data Providers (FDP) threshold.
static const _defaultFdpThreshold = 2;

/// Constructor for [FeatureEnvyParameters] model.
const FeatureEnvyParameters({
required this.exclude,
required this.atfdThreshold,
required this.laaThreshold,
required this.fdpThreshold,
});

/// Empty [FeatureEnvyParameters] model with default values.
FeatureEnvyParameters.empty()
: exclude = ExcludedIdentifiersListParameter(exclude: []),
atfdThreshold = _defaultAtfdThreshold,
laaThreshold = _defaultLaaThreshold,
fdpThreshold = _defaultFdpThreshold;

/// Creates a [FeatureEnvyParameters] model from JSON data.
FeatureEnvyParameters.fromJson(Map<String, Object?> json)
: exclude = ExcludedIdentifiersListParameter.defaultFromJson(json),
atfdThreshold = json['atfd_threshold'] as int? ?? _defaultAtfdThreshold,
laaThreshold =
(json['laa_threshold'] as num?)?.toDouble() ?? _defaultLaaThreshold,
fdpThreshold = json['fdp_threshold'] as int? ?? _defaultFdpThreshold;
}
13 changes: 13 additions & 0 deletions lib/src/lints/feature_envy/models/project_class_cache.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import 'package:analyzer/dart/element/element.dart';
import 'package:solid_lints/src/utils/node_utils.dart';

/// A cache for checking if an [InterfaceElement] belongs to the analyzed
/// project.
class ProjectClassCache {
final _cache = <InterfaceElement, bool>{};

/// Returns `true` if [element] belongs to the analyzed project.
/// Results are cached for better performance during static analysis.
bool isProjectClass(InterfaceElement element) =>
_cache.putIfAbsent(element, () => element.isFromProject);
}
Loading