Merge pull request #3528 from cake-tech/money-to-string-lint

This commit is contained in:
malik1004x
2026-08-17 08:57:25 +02:00
committed by GitHub
3 changed files with 159 additions and 0 deletions
+8
View File
@@ -3,6 +3,8 @@ import "package:analysis_server_plugin/registry.dart";
import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart";
import "package:cw_custom_lints/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart";
import "package:cw_custom_lints/modern_button_semantics/modern_button_semantics_rule.dart";
import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_fix.dart";
import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_rule.dart";
import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart";
import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart";
import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart";
@@ -20,7 +22,13 @@ class CwCustomLintsPlugin extends Plugin {
registry.registerWarningRule(HttpForceProxyRule());
registry.registerWarningRule(ModernButtonSemanticsRule());
registry.registerWarningRule(ModalTopBarSemanticsRule());
registry.registerWarningRule(MoneyAmountToStringRule());
registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new);
registry.registerFixForRule(MoneyAmountToStringRule.code, ReplaceWithMoneyToString.new);
registry.registerFixForRule(
MoneyAmountToStringRule.code,
ReplaceWithMoneyToStringInBaseUnit.new,
);
}
}
@@ -0,0 +1,66 @@
import "package:analysis_server_plugin/edit/dart/correction_producer.dart";
import "package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart";
import "package:analyzer/dart/ast/ast.dart";
import "package:analyzer_plugin/utilities/change_builder/change_builder_core.dart";
import "package:analyzer_plugin/utilities/fixes/fixes.dart";
import "package:analyzer_plugin/utilities/range_factory.dart";
import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_rule.dart";
abstract class _ReplaceMoneyAmountToString extends ResolvedCorrectionProducer {
_ReplaceMoneyAmountToString({required super.context});
String get replacement;
@override
CorrectionApplicability get applicability => CorrectionApplicability.singleLocation;
@override
Future<void> compute(ChangeBuilder builder) async {
final invocation = node.thisOrAncestorOfType<MethodInvocation>();
if (invocation == null) {
return;
}
final amount = moneyAmountReference(invocation.realTarget);
if (amount == null) {
return;
}
await builder.addDartFileEdit(
file,
(builder) => builder.addSimpleReplacement(range.startEnd(amount, invocation), replacement),
);
}
}
class ReplaceWithMoneyToString extends _ReplaceMoneyAmountToString {
ReplaceWithMoneyToString({required super.context});
static const _fixKind = FixKind(
"dart.fix.replaceWithMoneyToString",
DartFixKindPriority.standard,
"Replace with toString() for a decimal amount",
);
@override
FixKind get fixKind => _fixKind;
@override
String get replacement => "toString()";
}
class ReplaceWithMoneyToStringInBaseUnit extends _ReplaceMoneyAmountToString {
ReplaceWithMoneyToStringInBaseUnit({required super.context});
static const _fixKind = FixKind(
"dart.fix.replaceWithMoneyToStringInBaseUnit",
DartFixKindPriority.standard,
"Replace with toStringWithPrecision(useBaseUnit: true) for base units",
);
@override
FixKind get fixKind => _fixKind;
@override
String get replacement => "toStringWithPrecision(useBaseUnit: true)";
}
@@ -0,0 +1,85 @@
import "package:analyzer/analysis_rule/analysis_rule.dart";
import "package:analyzer/analysis_rule/rule_context.dart";
import "package:analyzer/analysis_rule/rule_visitor_registry.dart";
import "package:analyzer/dart/ast/ast.dart";
import "package:analyzer/dart/ast/visitor.dart";
import "package:analyzer/dart/element/element.dart";
import "package:analyzer/error/error.dart";
class MoneyAmountToStringRule extends AnalysisRule {
MoneyAmountToStringRule()
: super(
name: "no_money_amount_to_string",
description:
"Money.amount is the internal BigInt, so stringifying it drops the decimal point.",
);
static const LintCode code = LintCode(
"no_money_amount_to_string",
"This stringifies the internal BigInt of Money instead of the Money itself, so the decimal point is dropped and the amount reads far larger than it is.",
correctionMessage:
"Call toString() on the Money for a decimal amount, or toStringWithPrecision(useBaseUnit: true) for base units.",
severity: DiagnosticSeverity.WARNING,
);
@override
LintCode get diagnosticCode => code;
@override
void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
registry.addMethodInvocation(this, _Visitor(this));
}
SimpleIdentifier? moneyAmountReference(Expression? expression) {
final identifier = switch (expression) {
PrefixedIdentifier() => expression.identifier,
PropertyAccess() => expression.propertyName,
SimpleIdentifier() => expression,
_ => null,
};
if (identifier == null || identifier.name != "amount") {
return null;
}
return _isDeclaredByMoney(identifier.element) ? identifier : null;
}
bool _isDeclaredByMoney(Element? element) {
if (element == null) {
return false;
}
final enclosing = element.enclosingElement;
if (enclosing is! InterfaceElement || enclosing.name != "Money") {
return false;
}
final libraryUri = element.library?.uri;
if (libraryUri == null || libraryUri.scheme != "package") {
return false;
}
final segments = libraryUri.pathSegments;
return segments.isNotEmpty && segments.first == "cw_core";
}
class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule);
final AnalysisRule rule;
@override
void visitMethodInvocation(MethodInvocation node) {
if (node.methodName.name != "toString" || node.argumentList.arguments.isNotEmpty) {
return;
}
if (moneyAmountReference(node.realTarget) == null) {
return;
}
rule.reportAtNode(node);
}
}