diff --git a/CHANGELOG.md b/CHANGELOG.md index e149bb4d36..2bf8eb7090 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * fix: export missing parts of public API. * feat: support `context.mounted` for [`use-setstate-synchronously`](https://dcm.dev/docs/individuals/rules/flutter/use-setstate-synchronously). +* feat: add `allow-only-once` option to [`no-magic-number`](https://dcm.dev/docs/individuals/rules/common/no-magic-number). ## 5.5.0 diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/config_parser.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/config_parser.dart index a67a8eca8a..d0d979dd18 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/config_parser.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/config_parser.dart @@ -3,9 +3,14 @@ part of 'no_magic_number_rule.dart'; class _ConfigParser { static const _allowedConfigName = 'allowed'; + static const _allowOnlyOnce = 'allow-only-once'; + static const _defaultMagicNumbers = [-1, 0, 1]; static Iterable parseAllowedNumbers(Map config) => (config[_allowedConfigName] as Iterable?)?.cast() ?? _defaultMagicNumbers; + + static bool parseAllowOnlyOnce(Map config) => + (config[_allowOnlyOnce] as bool?) ?? false; } diff --git a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule.dart b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule.dart index 139d9a3964..c3df7322f4 100644 --- a/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule.dart +++ b/lib/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule.dart @@ -21,9 +21,11 @@ class NoMagicNumberRule extends CommonRule { 'Avoid using magic numbers. Extract them to named constants or variables.'; final Iterable _allowedMagicNumbers; + final bool _allowOnlyOnce; NoMagicNumberRule([Map config = const {}]) : _allowedMagicNumbers = _ConfigParser.parseAllowedNumbers(config), + _allowOnlyOnce = _ConfigParser.parseAllowOnlyOnce(config), super( id: ruleId, severity: readSeverity(config, Severity.warning), @@ -45,7 +47,11 @@ class NoMagicNumberRule extends CommonRule { source.unit.visitChildren(visitor); - return visitor.literals + final literals = _allowOnlyOnce + ? _getNotSingleLiterals(visitor.literals) + : visitor.literals; + + return literals .where(_isMagicNumber) .where(_isNotInsideVariable) .where(_isNotInsideCollectionLiteral) @@ -62,6 +68,24 @@ class NoMagicNumberRule extends CommonRule { .toList(growable: false); } + Iterable _getNotSingleLiterals(Iterable literals) { + final literalsCount = {}; + for (final l in literals) { + if (l is IntegerLiteral) { + final value = l.value; + if (value != null) { + literalsCount.update(value, (count) => ++count, ifAbsent: () => 1); + } + } else if (l is DoubleLiteral) { + literalsCount.update(l.value, (count) => ++count, ifAbsent: () => 1); + } + } + + return literals.where((l) => + l is IntegerLiteral && literalsCount[l.value] != 1 || + l is DoubleLiteral && literalsCount[l.value] != 1); + } + bool _isMagicNumber(Literal l) => (l is DoubleLiteral && !_allowedMagicNumbers.contains(l.value)) || (l is IntegerLiteral && !_allowedMagicNumbers.contains(l.value)); diff --git a/test/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule_test.dart b/test/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule_test.dart index 79ffe1034c..5d98eee359 100644 --- a/test/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule_test.dart +++ b/test/src/analyzers/lint_analyzer/rules/rules_list/no_magic_number/no_magic_number_rule_test.dart @@ -68,6 +68,22 @@ void main() { RuleTestHelper.verifyNoIssues(issues); }); + test('reports magic numbers used more than once', () async { + final unit = await RuleTestHelper.resolveFromFile(_incorrectExamplePath); + final config = { + 'allow-only-once': true, + }; + + final issues = NoMagicNumberRule(config).check(unit); + + RuleTestHelper.verifyIssues( + issues: issues, + startLines: [2, 4], + startColumns: [28, 25], + locationTexts: ['12', '12'], + ); + }); + test( 'reports magic numbers in objects in widget array structures', () async { diff --git a/website/docs/rules/common/no-magic-number.mdx b/website/docs/rules/common/no-magic-number.mdx index 387bb6b261..79121853b5 100644 --- a/website/docs/rules/common/no-magic-number.mdx +++ b/website/docs/rules/common/no-magic-number.mdx @@ -56,4 +56,5 @@ dart_code_metrics: ... - no-magic-number: allowed: [3.14, 100, 12] + allow-only-once: true ```