Skip to content

Commit 4f4ff18

Browse files
committed
Added use_nearest_context rule
1 parent 7c8b697 commit 4f4ff18

File tree

6 files changed

+268
-0
lines changed

6 files changed

+268
-0
lines changed

lib/solid_lints.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart';
2828
import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart';
2929
import 'package:solid_lints/src/lints/prefer_match_file_name/prefer_match_file_name_rule.dart';
3030
import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart';
31+
import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart';
3132
import 'package:solid_lints/src/models/solid_lint_rule.dart';
3233

3334
/// Creates a plugin for our custom linter
@@ -65,6 +66,7 @@ class _SolidLints extends PluginBase {
6566
AvoidDebugPrintInReleaseRule.createRule(configs),
6667
PreferEarlyReturnRule.createRule(configs),
6768
AvoidFinalWithGetterRule.createRule(configs),
69+
UseNearestContextRule.createRule(configs),
6870
];
6971

7072
// Return only enabled rules
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
part of '../use_nearest_context_rule.dart';
2+
3+
/// A Quick fix for `use_nearest_context` rule
4+
/// Suggests to renaming the nearest BuildContext variable
5+
/// to the one that is being used
6+
class _UseNearestContextFix extends DartFix {
7+
static const _replaceComment = "Rename the BuildContext variable";
8+
9+
@override
10+
void run(
11+
CustomLintResolver resolver,
12+
ChangeReporter reporter,
13+
CustomLintContext context,
14+
AnalysisError analysisError,
15+
List<AnalysisError> others,
16+
) {
17+
context.registry.addFunctionDeclaration((node) {
18+
final statementInfo = analysisError.data as StatementInfo?;
19+
if (statementInfo == null) return;
20+
final parameterName = statementInfo.parameter.name;
21+
if (parameterName == null) return;
22+
if (node.sourceRange.intersects(parameterName.sourceRange)) {
23+
_addReplacement(
24+
reporter,
25+
parameterName,
26+
statementInfo.name,
27+
);
28+
}
29+
});
30+
}
31+
32+
void _addReplacement(
33+
ChangeReporter reporter,
34+
Token? token,
35+
String correction,
36+
) {
37+
if (token == null) return;
38+
final changeBuilder = reporter.createChangeBuilder(
39+
message: _replaceComment,
40+
priority: 1,
41+
);
42+
43+
changeBuilder.addDartFileEdit((builder) {
44+
builder.addSimpleReplacement(
45+
token.sourceRange,
46+
correction,
47+
);
48+
});
49+
}
50+
}
51+
52+
/// Data class contains info required for fix
53+
class StatementInfo {
54+
/// Creates instance of an [StatementInfo]
55+
const StatementInfo({
56+
required this.name,
57+
required this.parameter,
58+
});
59+
60+
/// Variable name
61+
final String name;
62+
63+
/// BuildContext parament
64+
final SimpleFormalParameter parameter;
65+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'package:solid_lints/src/common/parameters/excluded_identifiers_list_parameter.dart';
2+
3+
/// A data model class that represents the "use nearest context" input
4+
/// parameters.
5+
class UseNearestContextParameters {
6+
/// A list of methods that should be excluded from the lint.
7+
final ExcludedIdentifiersListParameter exclude;
8+
9+
/// Constructor for [UseNearestContextParameters] model
10+
const UseNearestContextParameters({
11+
required this.exclude,
12+
});
13+
14+
/// Method for creating from json data
15+
factory UseNearestContextParameters.fromJson(Map<String, Object?> json) =>
16+
UseNearestContextParameters(
17+
exclude: ExcludedIdentifiersListParameter.defaultFromJson(json),
18+
);
19+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// ignore_for_file: avoid_print, lines_longer_than_80_chars
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer/dart/ast/token.dart';
5+
import 'package:analyzer/error/error.dart';
6+
import 'package:analyzer/error/listener.dart';
7+
import 'package:custom_lint_builder/custom_lint_builder.dart';
8+
import 'package:solid_lints/src/lints/use_nearest_context/models/use_nearest_context_parameters.dart';
9+
import 'package:solid_lints/src/models/rule_config.dart';
10+
import 'package:solid_lints/src/models/solid_lint_rule.dart';
11+
import 'package:solid_lints/src/utils/types_utils.dart';
12+
13+
part 'fixes/use_nearest_context_fix.dart';
14+
15+
/// A rule which checks that we use BuildContext from the nearest available
16+
/// scope.
17+
///
18+
/// ### Example:
19+
/// #### BAD:
20+
/// ```dart
21+
/// class SomeWidget extends StatefulWidget {
22+
/// ...
23+
/// }
24+
///
25+
/// class _SomeWidgetState extends State<SomeWidget> {
26+
/// ...
27+
/// void _showDialog() {
28+
/// showModalBottomSheet(
29+
/// context: context,
30+
/// builder: (BuildContext _) {
31+
/// final someProvider = context.watch<SomeProvider>(); // LINT, BuildContext is used not from the nearest available scope
32+
///
33+
/// return const SizedBox.shrink();
34+
/// },
35+
/// );
36+
/// }
37+
/// }
38+
/// ```
39+
/// #### GOOD:
40+
/// ```dart
41+
/// class SomeWidget extends StatefulWidget {
42+
/// ...
43+
/// }
44+
///
45+
/// class _SomeWidgetState extends State<SomeWidget> {
46+
/// ...
47+
/// void _showDialog() {
48+
/// showModalBottomSheet(
49+
/// context: context,
50+
/// builder: (BuildContext context)
51+
/// final someProvider = context.watch<SomeProvider>(); // OK
52+
///
53+
/// return const SizedBox.shrink();
54+
/// },
55+
/// );
56+
/// }
57+
/// }
58+
/// ```
59+
///
60+
class UseNearestContextRule extends SolidLintRule<UseNearestContextParameters> {
61+
/// This lint rule represents the error if BuildContext is used not from the
62+
/// nearest available scope
63+
static const lintName = 'use_nearest_context';
64+
65+
UseNearestContextRule._(super.rule);
66+
67+
/// Creates a new instance of [UseNearestContextRule]
68+
/// based on the lint configuration.
69+
factory UseNearestContextRule.createRule(CustomLintConfigs configs) {
70+
final rule = RuleConfig(
71+
configs: configs,
72+
name: lintName,
73+
paramsParser: UseNearestContextParameters.fromJson,
74+
problemMessage: (value) =>
75+
'BuildContext is used not from the nearest available scope. '
76+
'Consider renaming the nearest BuildContext parameter.',
77+
);
78+
79+
return UseNearestContextRule._(rule);
80+
}
81+
82+
@override
83+
void run(
84+
CustomLintResolver resolver,
85+
ErrorReporter reporter,
86+
CustomLintContext context,
87+
) {
88+
context.registry.addSimpleIdentifier((node) {
89+
if (!isBuildContext(node.staticType)) return;
90+
91+
final closestBuildContext = _findClosestBuildContext(node);
92+
if (closestBuildContext == null) return;
93+
if (closestBuildContext.name?.lexeme != node.name) {
94+
reporter.atNode(
95+
node,
96+
code,
97+
data: StatementInfo(
98+
name: node.name,
99+
parameter: closestBuildContext,
100+
),
101+
);
102+
}
103+
});
104+
}
105+
106+
SimpleFormalParameter? _findClosestBuildContext(SimpleIdentifier node) {
107+
AstNode? current = node.parent;
108+
109+
while (current != null) {
110+
if (current is FunctionExpression) {
111+
final functionParams = current.parameters?.parameters ?? [];
112+
for (final param in functionParams) {
113+
if (param is SimpleFormalParameter &&
114+
isBuildContext(param.declaredElement?.type)) {
115+
return param;
116+
}
117+
}
118+
}
119+
current = current.parent;
120+
}
121+
return null;
122+
}
123+
124+
@override
125+
List<Fix> getFixes() => [_UseNearestContextFix()];
126+
}

lint_test/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,4 @@ custom_lint:
8181
- prefer_match_file_name
8282
- proper_super_calls
8383
- avoid_final_with_getter
84+
- use_nearest_context
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// ignore_for_file: avoid_unused_parameters
2+
import 'package:flutter/material.dart';
3+
4+
/// Check the `use_nearest_context` rule
5+
void showDialog(BuildContext context) {
6+
final outerContext = context;
7+
8+
showModalBottomSheet(
9+
context: context,
10+
builder: (BuildContext _) {
11+
/// expect_lint: use_nearest_context
12+
return SizedBox.fromSize(size: outerContext.size);
13+
},
14+
);
15+
16+
showModalBottomSheet(
17+
context: context,
18+
builder: (BuildContext _) {
19+
/// expect_lint: use_nearest_context
20+
return SizedBox.fromSize(size: context.size);
21+
},
22+
);
23+
24+
showModalBottomSheet(
25+
context: context,
26+
builder: (_) {
27+
/// expect_lint: use_nearest_context
28+
return SizedBox.fromSize(size: context.size);
29+
},
30+
);
31+
32+
showModalBottomSheet(
33+
context: context,
34+
builder: (BuildContext innerContext) {
35+
/// expect_lint: use_nearest_context
36+
return SizedBox.fromSize(size: context.size);
37+
},
38+
);
39+
40+
showModalBottomSheet(
41+
context: context,
42+
builder: (BuildContext innerContext) {
43+
///Allowed
44+
return SizedBox.fromSize(size: innerContext.size);
45+
});
46+
47+
showModalBottomSheet(
48+
///Allowed
49+
context: context,
50+
builder: (BuildContext context) {
51+
///Allowed
52+
return SizedBox.fromSize(size: context.size);
53+
},
54+
);
55+
}

0 commit comments

Comments
 (0)