Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Update min Dart SDK constraint to 3.5.0
- Update `analyzer` dependency to 7.1.0
- Update `custom_lint_builder` dependency to 0.7.1
- Added `use_nearest_context` rule

## 0.2.3

Expand Down
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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 @@ -65,6 +66,7 @@ class _SolidLints extends PluginBase {
AvoidDebugPrintInReleaseRule.createRule(configs),
PreferEarlyReturnRule.createRule(configs),
AvoidFinalWithGetterRule.createRule(configs),
UseNearestContextRule.createRule(configs),
];

// Return only enabled rules
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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 the BuildContext variable";

@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
context.registry.addFunctionDeclaration((node) {
final statementInfo = analysisError.data as StatementInfo?;
if (statementInfo == null) return;
final parameterName = statementInfo.parameter.name;
if (parameterName == null) return;
if (node.sourceRange.intersects(parameterName.sourceRange)) {
_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 contains info required for fix
class StatementInfo {
/// Creates instance of an [StatementInfo]
const StatementInfo({
required this.name,
required this.parameter,
});

/// Variable name
final String name;

/// BuildContext parament
final SimpleFormalParameter parameter;
Comment on lines +60 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The documentation for the StatementInfo class could be more descriptive to enhance clarity for future maintainers.

Specifically:

  • For the name field, "Variable name" is a bit vague. It represents the identifier of the BuildContext that was used (from an outer scope), and it's the target name for the renaming operation.
  • For the parameter field, "BuildContext parament" contains a typo and could better describe that this is the AST node of the nearest BuildContext parameter that will be renamed.

Could we update these comments for better precision?

  /// The identifier name of the BuildContext that was used from an outer scope.
  /// The nearest BuildContext parameter will be renamed to this name.
  final String name;

  /// The [SimpleFormalParameter] node of the BuildContext in the nearest scope.
  /// This is the parameter that will be renamed.
  final SimpleFormalParameter parameter;

}
124 changes: 124 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,124 @@
// 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/error/error.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';

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) =>
'BuildContext is used not from the nearest available scope. '
'Consider renaming the nearest BuildContext parameter.',
);

return UseNearestContextRule._(rule);
}

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

final closestBuildContext = _findClosestBuildContext(node);
if (closestBuildContext == null) return;
if (closestBuildContext.name?.lexeme != node.name) {
reporter.atNode(
node,
code,
data: StatementInfo(
name: node.name,
parameter: closestBuildContext,
),
);
}
});
}

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) {
if (param is SimpleFormalParameter &&
isBuildContext(param.declaredElement?.type)) {
return param;
}
}
}
current = current.parent;
}
return null;
}

@override
List<Fix> getFixes() => [_UseNearestContextFix()];
}
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ custom_lint:
- prefer_match_file_name
- proper_super_calls
- avoid_final_with_getter
- use_nearest_context
60 changes: 60 additions & 0 deletions lint_test/use_nearest_context_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// ignore_for_file: avoid_unused_parameters, unused_local_variable
import 'package:flutter/material.dart';

/// Check the `use_nearest_context` rule
void showDialog(BuildContext context) {
final outerContext = context;

showModalBottomSheet(
context: context,
builder: (BuildContext _) {
/// expect_lint: use_nearest_context
return SizedBox.fromSize(size: outerContext.size);
},
);

showModalBottomSheet(
context: context,
builder: (BuildContext _) {
/// expect_lint: use_nearest_context
return SizedBox.fromSize(size: context.size);
},
);

final fun = ({required BuildContext context}) {
/// expect_lint: use_nearest_context
outerContext.mounted;
};

showModalBottomSheet(
context: context,
builder: (_) {
/// expect_lint: use_nearest_context
return SizedBox.fromSize(size: context.size);
},
);

showModalBottomSheet(
context: context,
builder: (BuildContext innerContext) {
/// expect_lint: use_nearest_context
return SizedBox.fromSize(size: context.size);
},
);

showModalBottomSheet(
context: context,
builder: (BuildContext innerContext) {
///Allowed
return SizedBox.fromSize(size: innerContext.size);
});

showModalBottomSheet(
///Allowed
context: context,
builder: (BuildContext context) {
///Allowed
return SizedBox.fromSize(size: context.size);
},
);
}