add lint and quick-fixes for Money.amount.toString()
Robert Malikowski committed
Aug 15, 2026 at 14:34 UTC
8d6b3cd85d8b28de39c77cd2676067d0850bbed1
3 files changed
+159
cw_custom_lints/lib/main.dart
+8
@@ -3,6 +3,8 @@ import "package:analysis_server_plugin/registry.dart";
3
import "package:cw_custom_lints/http_force_proxy/http_force_proxy_rule.dart";
4
import "package:cw_custom_lints/modal_top_bar_semantics/modal_top_bar_semantics_rule.dart";
5
import "package:cw_custom_lints/modern_button_semantics/modern_button_semantics_rule.dart";
6
+import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_fix.dart";
7
+import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_rule.dart";
8
import "package:cw_custom_lints/print_verbose/print_verbose_fix.dart";
9
import "package:cw_custom_lints/print_verbose/print_verbose_rule.dart";
10
import "package:cw_custom_lints/restricted_imports/restricted_imports_rule.dart";
@@ -20,7 +22,13 @@ class CwCustomLintsPlugin extends Plugin {
22
registry.registerWarningRule(HttpForceProxyRule());
23
registry.registerWarningRule(ModernButtonSemanticsRule());
24
registry.registerWarningRule(ModalTopBarSemanticsRule());
25
+ registry.registerWarningRule(MoneyAmountToStringRule());
26
27
registry.registerFixForRule(PrintVerboseRule.code, ReplaceWithPrintV.new);
28
+ registry.registerFixForRule(MoneyAmountToStringRule.code, ReplaceWithMoneyToString.new);
29
+ registry.registerFixForRule(
30
+ MoneyAmountToStringRule.code,
31
+ ReplaceWithMoneyToStringInBaseUnit.new,
32
+ );
33
}
34
}
cw_custom_lints/lib/money_amount_to_string/money_amount_to_string_fix.dart
new
+66
@@ -0,0 +1,66 @@
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:analyzer_plugin/utilities/range_factory.dart";
7
+import "package:cw_custom_lints/money_amount_to_string/money_amount_to_string_rule.dart";
8
+
9
+abstract class _ReplaceMoneyAmountToString extends ResolvedCorrectionProducer {
10
+ _ReplaceMoneyAmountToString({required super.context});
11
+
12
+ String get replacement;
13
+
14
+ @override
15
+ CorrectionApplicability get applicability => CorrectionApplicability.singleLocation;
16
+
17
+ @override
18
+ Future<void> compute(ChangeBuilder builder) async {
19
+ final invocation = node.thisOrAncestorOfType<MethodInvocation>();
20
+ if (invocation == null) {
21
+ return;
22
+ }
23
+
24
+ final amount = moneyAmountReference(invocation.realTarget);
25
+ if (amount == null) {
26
+ return;
27
+ }
28
+
29
+ await builder.addDartFileEdit(
30
+ file,
31
+ (builder) => builder.addSimpleReplacement(range.startEnd(amount, invocation), replacement),
32
+ );
33
+ }
34
+}
35
+
36
+class ReplaceWithMoneyToString extends _ReplaceMoneyAmountToString {
37
+ ReplaceWithMoneyToString({required super.context});
38
+
39
+ static const _fixKind = FixKind(
40
+ "dart.fix.replaceWithMoneyToString",
41
+ DartFixKindPriority.standard,
42
+ "Replace with toString() for a decimal amount",
43
+ );
44
+
45
+ @override
46
+ FixKind get fixKind => _fixKind;
47
+
48
+ @override
49
+ String get replacement => "toString()";
50
+}
51
+
52
+class ReplaceWithMoneyToStringInBaseUnit extends _ReplaceMoneyAmountToString {
53
+ ReplaceWithMoneyToStringInBaseUnit({required super.context});
54
+
55
+ static const _fixKind = FixKind(
56
+ "dart.fix.replaceWithMoneyToStringInBaseUnit",
57
+ DartFixKindPriority.standard,
58
+ "Replace with toStringWithPrecision(useBaseUnit: true) for base units",
59
+ );
60
+
61
+ @override
62
+ FixKind get fixKind => _fixKind;
63
+
64
+ @override
65
+ String get replacement => "toStringWithPrecision(useBaseUnit: true)";
66
+}
cw_custom_lints/lib/money_amount_to_string/money_amount_to_string_rule.dart
new
+85
@@ -0,0 +1,85 @@
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/dart/ast/ast.dart";
5
+import "package:analyzer/dart/ast/visitor.dart";
6
+import "package:analyzer/dart/element/element.dart";
7
+import "package:analyzer/error/error.dart";
8
+
9
+class MoneyAmountToStringRule extends AnalysisRule {
10
+ MoneyAmountToStringRule()
11
+ : super(
12
+ name: "no_money_amount_to_string",
13
+ description:
14
+ "Money.amount is the internal BigInt, so stringifying it drops the decimal point.",
15
+ );
16
+
17
+ static const LintCode code = LintCode(
18
+ "no_money_amount_to_string",
19
+ "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.",
20
+ correctionMessage:
21
+ "Call toString() on the Money for a decimal amount, or toStringWithPrecision(useBaseUnit: true) for base units.",
22
+ severity: DiagnosticSeverity.WARNING,
23
+ );
24
+
25
+ @override
26
+ LintCode get diagnosticCode => code;
27
+
28
+ @override
29
+ void registerNodeProcessors(RuleVisitorRegistry registry, RuleContext context) =>
30
+ registry.addMethodInvocation(this, _Visitor(this));
31
+}
32
+
33
+
34
+SimpleIdentifier? moneyAmountReference(Expression? expression) {
35
+ final identifier = switch (expression) {
36
+ PrefixedIdentifier() => expression.identifier,
37
+ PropertyAccess() => expression.propertyName,
38
+ SimpleIdentifier() => expression,
39
+ _ => null,
40
+ };
41
+
42
+ if (identifier == null || identifier.name != "amount") {
43
+ return null;
44
+ }
45
+
46
+ return _isDeclaredByMoney(identifier.element) ? identifier : null;
47
+}
48
+
49
+bool _isDeclaredByMoney(Element? element) {
50
+ if (element == null) {
51
+ return false;
52
+ }
53
+
54
+ final enclosing = element.enclosingElement;
55
+ if (enclosing is! InterfaceElement || enclosing.name != "Money") {
56
+ return false;
57
+ }
58
+
59
+ final libraryUri = element.library?.uri;
60
+ if (libraryUri == null || libraryUri.scheme != "package") {
61
+ return false;
62
+ }
63
+
64
+ final segments = libraryUri.pathSegments;
65
+ return segments.isNotEmpty && segments.first == "cw_core";
66
+}
67
+
68
+class _Visitor extends SimpleAstVisitor<void> {
69
+ _Visitor(this.rule);
70
+
71
+ final AnalysisRule rule;
72
+
73
+ @override
74
+ void visitMethodInvocation(MethodInvocation node) {
75
+ if (node.methodName.name != "toString" || node.argumentList.arguments.isNotEmpty) {
76
+ return;
77
+ }
78
+
79
+ if (moneyAmountReference(node.realTarget) == null) {
80
+ return;
81
+ }
82
+
83
+ rule.reportAtNode(node);
84
+ }
85
+}