Skip to content
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.3.4

- Added `use_nearest_context` rule.

## 0.3.3

- Fix pub.dev analysis issue
Expand Down
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart';
import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart';
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
import 'package:solid_lints/src/models/solid_lint_rule.dart';

/// Creates a plugin for our custom linter
Expand Down Expand Up @@ -69,6 +70,7 @@ class _SolidLints extends PluginBase {
AvoidFinalWithGetterRule.createRule(configs),
NamedParametersOrderingRule.createRule(configs),
AvoidUnnecessaryReturnVariableRule.createRule(configs),
UseNearestContextRule.createRule(configs),
];

// Return only enabled rules
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
part of '../use_nearest_context_rule.dart';

/// A Quick fix for `use_nearest_context` rule
/// Suggests to renaming the nearest BuildContext variable
/// to the one that is being used
class _UseNearestContextFix extends DartFix {
static const _replaceComment = "Rename nearest BuildContext parameter";

final Expando<StatementInfo> _diagnosticsInfoExpando;

_UseNearestContextFix(this._diagnosticsInfoExpando);

@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
Diagnostic diagnostic,
List<Diagnostic> others,
) {
final statementInfo = _diagnosticsInfoExpando[diagnostic];
if (statementInfo == null) return;
final parameterName = statementInfo.parameter.name;
if (parameterName == null) return;

_addReplacement(reporter, parameterName, statementInfo.name);
}

void _addReplacement(
ChangeReporter reporter,
Token? token,
String correction,
) {
if (token == null) return;
final changeBuilder = reporter.createChangeBuilder(
message: _replaceComment,
priority: 1,
);

changeBuilder.addDartFileEdit((builder) {
builder.addSimpleReplacement(
token.sourceRange,
correction,
);
});
}
}

/// Data class that holds info required for the [_UseNearestContextFix].
class StatementInfo {
/// Creates instance of a [StatementInfo].
const StatementInfo({
required this.name,
required this.parameter,
});

/// The name of the outer BuildContext variable that was used
/// instead of the nearest one.
final String name;

/// The nearest [SimpleFormalParameter] of type BuildContext
/// that should have been used instead.
final SimpleFormalParameter parameter;
}
160 changes: 160 additions & 0 deletions lib/src/lints/use_nearest_context/use_nearest_context_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// ignore_for_file: avoid_print, lines_longer_than_80_chars

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/diagnostic/diagnostic.dart';
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.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';

part 'fixes/use_nearest_context_fix.dart';

/// A rule which checks that we use BuildContext from the nearest available
/// scope.
///
/// ### Example:
/// #### BAD:
/// ```dart
/// class SomeWidget extends StatefulWidget {
/// ...
/// }
///
/// class _SomeWidgetState extends State<SomeWidget> {
/// ...
/// void _showDialog() {
/// showModalBottomSheet(
/// context: context,
/// builder: (BuildContext _) {
/// final someProvider = context.watch<SomeProvider>(); // LINT, BuildContext is used not from the nearest available scope
///
/// return const SizedBox.shrink();
/// },
/// );
/// }
/// }
/// ```
/// #### GOOD:
/// ```dart
/// class SomeWidget extends StatefulWidget {
/// ...
/// }
///
/// class _SomeWidgetState extends State<SomeWidget> {
/// ...
/// void _showDialog() {
/// showModalBottomSheet(
/// context: context,
/// builder: (BuildContext context)
/// final someProvider = context.watch<SomeProvider>(); // OK
///
/// return const SizedBox.shrink();
/// },
/// );
/// }
/// }
/// ```
///
class UseNearestContextRule extends SolidLintRule {
/// This lint rule represents the error if BuildContext is used not from the
/// nearest available scope
static const lintName = 'use_nearest_context';

final _diagnosticsInfoExpando = Expando<StatementInfo>();

UseNearestContextRule._(super.rule);

/// Creates a new instance of [UseNearestContextRule]
/// based on the lint configuration.
factory UseNearestContextRule.createRule(CustomLintConfigs configs) {
final rule = RuleConfig(
configs: configs,
name: lintName,
problemMessage: (value) =>
'Use the nearest BuildContext parameter instead of the outer one.',
);

return UseNearestContextRule._(rule);
}

@override
void run(
CustomLintResolver resolver,
DiagnosticReporter reporter,
CustomLintContext context,
) {
context.registry.addSimpleIdentifier((node) {
if (!isBuildContext(node.staticType)) return;
if (_isPropertyOfOtherObject(node)) return;

final closestBuildContext = _findClosestBuildContext(node);
if (closestBuildContext == null) return;
if (closestBuildContext.name?.lexeme != node.name) {
if (_isDeclaredInNearestScope(node, closestBuildContext)) return;

final diagnostic = reporter.atNode(node, code);
_diagnosticsInfoExpando[diagnostic] = StatementInfo(
name: node.name,
parameter: closestBuildContext,
);
}
});
}
Comment thread
solid-illiaaihistov marked this conversation as resolved.

/// Returns `true` if [node] is a property accessed on another object
/// (e.g. `state.context`), but not on `this` (e.g. `this.context`).
bool _isPropertyOfOtherObject(SimpleIdentifier node) {
final parent = node.parent;
if (parent is PrefixedIdentifier && node == parent.identifier) {
return true;
}
if (parent is PropertyAccess && node == parent.propertyName) {
return parent.target is! ThisExpression;
}
return false;
}

/// Returns `true` if [node] refers to a variable declared inside the body
/// of the function that owns [closestParam] (i.e. a local variable
/// in the same scope, like `final localCtx = innerContext;`).
bool _isDeclaredInNearestScope(
SimpleIdentifier node,
SimpleFormalParameter closestParam,
) {
final element = node.element;
if (element is! LocalVariableElement) return false;

final nearestFunction = closestParam.parent?.parent;
if (nearestFunction is! FunctionExpression) return false;

final body = nearestFunction.body;
final declOffset = element.firstFragment.nameOffset;
if (declOffset == null) return false;
return declOffset >= body.offset && declOffset < body.end;
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}

SimpleFormalParameter? _findClosestBuildContext(SimpleIdentifier node) {
AstNode? current = node.parent;

while (current != null) {
if (current is FunctionExpression) {
final functionParams = current.parameters?.parameters ?? [];
for (final param in functionParams) {
final actualParam =
param is DefaultFormalParameter ? param.parameter : param;
if (actualParam is SimpleFormalParameter &&
isBuildContext(actualParam.declaredFragment?.element.type)) {
return actualParam;
Comment thread
solid-illiaaihistov marked this conversation as resolved.
}
}
}
current = current.parent;
}
return null;
}

@override
List<Fix> getFixes() => [_UseNearestContextFix(_diagnosticsInfoExpando)];
}
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,4 @@ custom_lint:
- nullable
- default
- avoid_unnecessary_return_variable
- use_nearest_context
Loading
Loading