Skip to content

Commit 8736521

Browse files
solid-illiaaihistovIllia Aihistov
andauthored
feat: add use_nearest_context rule (#293)
* feat: add use_nearest_context rule (#190) * fix: separate rename/replace quick fix applicability for use_nearest_context * refactor: simplify use_nearest_context test files * refactor: migrate use_nearest_context tests to AutoTestLintOffsets * refactor: simplify logic in use_nearest_context_visitor * refactor: move AST identifier utilities to node_utils and expose as SimpleIdentifier extension --------- Co-authored-by: Illia Aihistov <illia.aihistov-us@solid.software>
1 parent a978a20 commit 8736521

11 files changed

Lines changed: 661 additions & 3 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- feat!: migrate to analyzer_server_plugin
44
- refactor: replace `avoid_unnecessary_type_casts` rule with dart analyzer's `unnecessary_cast`
5+
- Added `use_nearest_context` rule.
56

67
## 0.3.3
78

lib/main.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import 'package:solid_lints/src/lints/avoid_unused_parameters/avoid_unused_param
1313
import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart';
1414
import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart';
1515
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
16+
import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart';
17+
import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart';
18+
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
1619

1720
/// The entry point for the Solid Lints analyser server plugin.
1821
///
@@ -50,6 +53,7 @@ class SolidLintsPlugin extends Plugin {
5053
AvoidUnusedParametersRule(
5154
analysisOptionsLoader: analysisLoader,
5255
),
56+
UseNearestContextRule(),
5357
];
5458

5559
for (final lintRule in lintRules) {
@@ -64,9 +68,20 @@ class SolidLintsPlugin extends Plugin {
6468
AvoidFinalWithGetterRule.code,
6569
AvoidFinalWithGetterFix.new,
6670
);
71+
6772
registry.registerFixForRule(
6873
avoidUnnecessaryTypeAssertionsRule.diagnosticCode,
6974
AvoidUnnecessaryTypeAssertionsFix.new,
7075
);
76+
77+
registry.registerFixForRule(
78+
UseNearestContextRule.code,
79+
RenameNearestContextParameterFix.new,
80+
);
81+
82+
registry.registerFixForRule(
83+
UseNearestContextRule.code,
84+
ReplaceWithNearestContextParameterFix.new,
85+
);
7186
}
7287
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
2+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
5+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
6+
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
7+
import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart';
8+
9+
/// A Quick fix for [UseNearestContextRule] rule
10+
/// Suggests to rename the nearest BuildContext parameter
11+
/// to the one that is being used.
12+
class RenameNearestContextParameterFix extends ResolvedCorrectionProducer {
13+
static const _renameParameterKind = FixKind(
14+
'solid_lints.fix.${UseNearestContextRule.lintName}.rename_parameter',
15+
DartFixKindPriority.standard,
16+
"Rename nearest BuildContext parameter",
17+
);
18+
19+
/// Creates a new instance of [RenameNearestContextParameterFix].
20+
RenameNearestContextParameterFix({required super.context});
21+
22+
@override
23+
FixKind get fixKind => _renameParameterKind;
24+
25+
@override
26+
CorrectionApplicability get applicability =>
27+
CorrectionApplicability.automatically;
28+
29+
@override
30+
Future<void> compute(ChangeBuilder builder) async {
31+
final identifierNode = node;
32+
if (identifierNode is! SimpleIdentifier) return;
33+
34+
// Do not offer renaming the parameter if this is an access on `this`
35+
// or `super`.
36+
final parent = identifierNode.parent;
37+
if (parent is PropertyAccess) {
38+
var target = parent.target;
39+
while (target is ParenthesizedExpression) {
40+
target = target.expression;
41+
}
42+
if (target is ThisExpression || target is SuperExpression) return;
43+
}
44+
45+
final closestBuildContext = findClosestBuildContext(identifierNode);
46+
if (closestBuildContext == null) return;
47+
48+
final parameterName = closestBuildContext.name;
49+
if (parameterName == null) return;
50+
51+
await builder.addDartFileEdit(file, (builder) {
52+
builder.addSimpleReplacement(
53+
parameterName.sourceRange,
54+
identifierNode.name,
55+
);
56+
});
57+
}
58+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
2+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
5+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
6+
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
7+
import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart';
8+
9+
/// A Quick fix for [UseNearestContextRule] rule
10+
/// Suggests to replace the outer BuildContext expression
11+
/// with the nearest available parameter.
12+
class ReplaceWithNearestContextParameterFix extends ResolvedCorrectionProducer {
13+
static const _replaceExpressionKind = FixKind(
14+
'solid_lints.fix.${UseNearestContextRule.lintName}.replace_expression',
15+
DartFixKindPriority.standard,
16+
"Replace with nearest BuildContext parameter",
17+
);
18+
19+
/// Creates a new instance of [ReplaceWithNearestContextParameterFix].
20+
ReplaceWithNearestContextParameterFix({required super.context});
21+
22+
@override
23+
FixKind get fixKind => _replaceExpressionKind;
24+
25+
@override
26+
CorrectionApplicability get applicability =>
27+
CorrectionApplicability.automatically;
28+
29+
@override
30+
Future<void> compute(ChangeBuilder builder) async {
31+
final errorNode = node;
32+
33+
final closestBuildContext = findClosestBuildContext(errorNode);
34+
if (closestBuildContext == null) return;
35+
36+
final parameterName = closestBuildContext.name?.lexeme;
37+
if (parameterName == null) return;
38+
39+
if (errorNode is ThisExpression) {
40+
await builder.addDartFileEdit(file, (builder) {
41+
builder.addSimpleReplacement(
42+
errorNode.sourceRange,
43+
parameterName,
44+
);
45+
});
46+
return;
47+
}
48+
49+
if (errorNode is SimpleIdentifier) {
50+
final parent = errorNode.parent;
51+
if (parent is PropertyAccess) {
52+
var target = parent.target;
53+
while (target is ParenthesizedExpression) {
54+
target = target.expression;
55+
}
56+
if (target is ThisExpression || target is SuperExpression) {
57+
await builder.addDartFileEdit(file, (builder) {
58+
builder.addSimpleReplacement(
59+
parent.sourceRange,
60+
parameterName,
61+
);
62+
});
63+
return;
64+
}
65+
}
66+
}
67+
}
68+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:analyzer/analysis_rule/analysis_rule.dart';
2+
import 'package:analyzer/analysis_rule/rule_context.dart';
3+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
4+
import 'package:analyzer/error/error.dart';
5+
import 'package:solid_lints/src/lints/use_nearest_context/visitors/use_nearest_context_visitor.dart';
6+
7+
/// A rule which checks that we use BuildContext from the nearest available
8+
/// scope.
9+
///
10+
/// ### Example:
11+
/// #### BAD:
12+
/// ```dart
13+
/// class SomeWidget extends StatefulWidget {
14+
/// ...
15+
/// }
16+
///
17+
/// class _SomeWidgetState extends State<SomeWidget> {
18+
/// ...
19+
/// void _showDialog() {
20+
/// showModalBottomSheet(
21+
/// context: context,
22+
/// builder: (BuildContext _) {
23+
/// final someProvider = context.watch<SomeProvider>(); // LINT, BuildContext is used not from the nearest available scope
24+
///
25+
/// return const SizedBox.shrink();
26+
/// },
27+
/// );
28+
/// }
29+
/// }
30+
/// ```
31+
/// #### GOOD:
32+
/// ```dart
33+
/// class SomeWidget extends StatefulWidget {
34+
/// ...
35+
/// }
36+
///
37+
/// class _SomeWidgetState extends State<SomeWidget> {
38+
/// ...
39+
/// void _showDialog() {
40+
/// showModalBottomSheet(
41+
/// context: context,
42+
/// builder: (BuildContext context)
43+
/// final someProvider = context.watch<SomeProvider>(); // OK
44+
///
45+
/// return const SizedBox.shrink();
46+
/// },
47+
/// );
48+
/// }
49+
/// }
50+
/// ```
51+
///
52+
class UseNearestContextRule extends AnalysisRule {
53+
/// This lint rule represents the error if BuildContext is used not from the
54+
/// nearest available scope
55+
static const lintName = 'use_nearest_context';
56+
57+
/// The code to report for a violation
58+
static const LintCode code = LintCode(
59+
lintName,
60+
'Use the nearest BuildContext parameter instead of the outer one.',
61+
);
62+
63+
/// Creates a new instance of [UseNearestContextRule].
64+
UseNearestContextRule()
65+
: super(
66+
name: lintName,
67+
description: code.problemMessage,
68+
);
69+
70+
@override
71+
LintCode get diagnosticCode => code;
72+
73+
@override
74+
void registerNodeProcessors(
75+
RuleVisitorRegistry registry,
76+
RuleContext context,
77+
) {
78+
final visitor = UseNearestContextVisitor(this);
79+
registry.addSimpleIdentifier(this, visitor);
80+
registry.addThisExpression(this, visitor);
81+
}
82+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:solid_lints/src/utils/types_utils.dart';
3+
4+
/// Finds the closest BuildContext parameter in the AST parent chain of [node].
5+
SimpleFormalParameter? findClosestBuildContext(AstNode node) {
6+
AstNode? current = node.parent;
7+
8+
while (current != null) {
9+
if (current is FunctionExpression) {
10+
final functionParams = current.parameters?.parameters ?? [];
11+
for (final param in functionParams) {
12+
final actualParam =
13+
param is DefaultFormalParameter ? param.parameter : param;
14+
if (actualParam is SimpleFormalParameter &&
15+
isBuildContext(actualParam.declaredFragment?.element.type)) {
16+
return actualParam;
17+
}
18+
}
19+
}
20+
current = current.parent;
21+
}
22+
return null;
23+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/visitor.dart';
3+
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
4+
import 'package:solid_lints/src/lints/use_nearest_context/utils/use_nearest_context_utils.dart';
5+
import 'package:solid_lints/src/utils/node_utils.dart';
6+
import 'package:solid_lints/src/utils/types_utils.dart';
7+
8+
/// A visitor for [UseNearestContextRule].
9+
class UseNearestContextVisitor extends SimpleAstVisitor<void> {
10+
final UseNearestContextRule _rule;
11+
12+
/// Creates a new instance of [UseNearestContextVisitor].
13+
UseNearestContextVisitor(this._rule);
14+
15+
@override
16+
void visitSimpleIdentifier(SimpleIdentifier node) {
17+
if (!isBuildContext(node.staticType)) return;
18+
if (node.isPropertyOfOtherObject) return;
19+
20+
final closestBuildContext = findClosestBuildContext(node);
21+
if (closestBuildContext == null) return;
22+
if (closestBuildContext.name?.lexeme == node.name) return;
23+
if (node.isDeclaredInSameFunction(as: closestBuildContext)) return;
24+
25+
_rule.reportAtNode(node);
26+
}
27+
28+
@override
29+
void visitThisExpression(ThisExpression node) {
30+
if (!isBuildContext(node.staticType)) return;
31+
32+
final closestBuildContext = findClosestBuildContext(node);
33+
if (closestBuildContext == null) return;
34+
35+
_rule.reportAtNode(node);
36+
}
37+
}

lib/src/utils/node_utils.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import 'package:analyzer/dart/ast/ast.dart';
22
import 'package:analyzer/dart/ast/token.dart';
3+
import 'package:analyzer/dart/element/element.dart';
4+
35

46
/// Check node is override method from its metadata
57
bool isOverride(List<Annotation> metadata) => metadata.any(
@@ -22,3 +24,40 @@ String humanReadableNodeType(AstNode? node) {
2224

2325
return 'Node';
2426
}
27+
28+
/// Extension on [SimpleIdentifier] to provide scope and property utility
29+
/// checks.
30+
extension SimpleIdentifierExtension on SimpleIdentifier {
31+
/// Returns `true` if this identifier is a property accessed on another
32+
/// object (e.g. `state.context`), but not on `this` (e.g. `this.context`).
33+
bool get isPropertyOfOtherObject {
34+
final parent = this.parent;
35+
if (parent is PrefixedIdentifier && this == parent.identifier) {
36+
return true;
37+
}
38+
if (parent is PropertyAccess && this == parent.propertyName) {
39+
var target = parent.target;
40+
while (target is ParenthesizedExpression) {
41+
target = target.expression;
42+
}
43+
return target is! ThisExpression && target is! SuperExpression;
44+
}
45+
return false;
46+
}
47+
48+
/// Returns `true` if this identifier refers to a variable declared inside
49+
/// the body of the function that owns [as] (i.e. a local variable in the
50+
/// same scope).
51+
bool isDeclaredInSameFunction({required SimpleFormalParameter as}) {
52+
final element = this.element;
53+
if (element is! LocalVariableElement) return false;
54+
55+
final nearestFunction = as.parent?.parent;
56+
if (nearestFunction is! FunctionExpression) return false;
57+
58+
final body = nearestFunction.body;
59+
final declOffset = element.firstFragment.nameOffset;
60+
if (declOffset == null) return false;
61+
return declOffset >= body.offset && declOffset < body.end;
62+
}
63+
}

lib/src/utils/types_utils.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,11 @@ bool isRowWidget(DartType? type) => type?.getDisplayString() == 'Row';
129129

130130
bool isPaddingWidget(DartType? type) => type?.getDisplayString() == 'Padding';
131131

132-
bool isBuildContext(DartType? type) =>
133-
type?.getDisplayString() == 'BuildContext';
132+
bool isBuildContext(DartType? type) {
133+
if (type == null) return false;
134+
final displayString = type.getDisplayString();
135+
return displayString == 'BuildContext' || displayString == 'BuildContext?';
136+
}
134137

135138
bool isGameWidget(DartType? type) => type?.getDisplayString() == 'GameWidget';
136139

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: solid_lints
22
description:
33
Lints for Dart and Flutter based on software industry standards and best
44
practices.
5-
version: 0.3.3
5+
version: 0.3.4
66
homepage: https://github.com/solid-software/solid_lints/
77
documentation: https://solid-software.github.io/solid_lints/docs/intro
88
topics: [lints, linter, lint, analysis, analyzer]

0 commit comments

Comments
 (0)