diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_unicode.dart b/packages/altive_lints/example/lints/avoid_hardcoded_unicode.dart new file mode 100644 index 0000000..4743d43 --- /dev/null +++ b/packages/altive_lints/example/lints/avoid_hardcoded_unicode.dart @@ -0,0 +1,12 @@ +// Check the `avoid_hardcoded_unicode` rule. + +// expect_lint: avoid_hardcoded_unicode +const japanese = 'こんにちは'; +// expect_lint: avoid_hardcoded_unicode +const russian = 'Ошибка'; +// expect_lint: avoid_hardcoded_unicode +const spanish = '¡Hola!'; + +const ascii = 'Hello!'; +const numbers = '12345'; +const symbols = '!@#%&*()'; diff --git a/packages/altive_lints/example/lints/avoid_hardcoded_unicode_test.dart b/packages/altive_lints/example/lints/avoid_hardcoded_unicode_test.dart new file mode 100644 index 0000000..6213cde --- /dev/null +++ b/packages/altive_lints/example/lints/avoid_hardcoded_unicode_test.dart @@ -0,0 +1,7 @@ +// Check the `avoid_hardcoded_unicode` rule. +// This file ends with the name `_test.dart`, +// so it should be exempt from the warning. + +const japanese = 'こんにちは'; +const russian = 'Ошибка'; +const ascii = 'Hello!'; diff --git a/packages/altive_lints/example/test/avoid_hardcoded_unicode.dart b/packages/altive_lints/example/test/avoid_hardcoded_unicode.dart new file mode 100644 index 0000000..6ec3acb --- /dev/null +++ b/packages/altive_lints/example/test/avoid_hardcoded_unicode.dart @@ -0,0 +1,6 @@ +// Check the `avoid_hardcoded_unicode` rule. +// It should exclude warnings for the entire `test` directory. + +const japanese = 'こんにちは'; +const russian = 'Ошибка'; +const ascii = 'Hello!'; diff --git a/packages/altive_lints/lib/all_lint_rules.yaml b/packages/altive_lints/lib/all_lint_rules.yaml index 7975e30..d3f2ff3 100644 --- a/packages/altive_lints/lib/all_lint_rules.yaml +++ b/packages/altive_lints/lib/all_lint_rules.yaml @@ -232,3 +232,4 @@ linter: - use_truncating_division - valid_regexps - void_checks + - avoid_hardcoded_unicode diff --git a/packages/altive_lints/lib/altive_lints.dart b/packages/altive_lints/lib/altive_lints.dart index e8591d9..6db3b2b 100644 --- a/packages/altive_lints/lib/altive_lints.dart +++ b/packages/altive_lints/lib/altive_lints.dart @@ -6,6 +6,7 @@ import 'src/assists/wrap_with_macro_template_document_comment.dart'; import 'src/lints/avoid_consecutive_sliver_to_box_adapter.dart'; import 'src/lints/avoid_hardcoded_color.dart'; import 'src/lints/avoid_hardcoded_japanese.dart'; +import 'src/lints/avoid_hardcoded_unicode.dart'; import 'src/lints/avoid_shrink_wrap_in_list_view.dart'; import 'src/lints/avoid_single_child.dart'; import 'src/lints/prefer_clock_now.dart'; @@ -22,6 +23,7 @@ class _AltivePlugin extends PluginBase { const AvoidConsecutiveSliverToBoxAdapter(), const AvoidHardcodedColor(), const AvoidHardcodedJapanese(), + const AvoidHardcodedUnicode(), const AvoidShrinkWrapInListView(), const AvoidSingleChild(), const PreferClockNow(), diff --git a/packages/altive_lints/lib/src/lints/avoid_hardcoded_unicode.dart b/packages/altive_lints/lib/src/lints/avoid_hardcoded_unicode.dart new file mode 100644 index 0000000..82c513e --- /dev/null +++ b/packages/altive_lints/lib/src/lints/avoid_hardcoded_unicode.dart @@ -0,0 +1,84 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +import '../utils/files_utils.dart'; + +/// An `avoid_hardcoded_unicode` rule which detects +/// and reports hardcoded string literals containing characters +/// outside a configurable ASCII range (default: 0x20-0x7E). +/// +/// This rule ensures that all user-facing text is +/// properly internationalized and avoids hardcoded non-ASCII text. +/// +/// ### Example +/// +/// #### BAD: +/// +/// ```dart +/// final message = 'こんにちは'; // LINT +/// print('Ошибка'); // LINT +/// print('¡Hola!'); // LINT +/// ``` +/// +/// #### GOOD: +/// +/// ```dart +/// final message = AppLocalizations.of(context).hello; +/// print(AppLocalizations.of(context).errorOccurred); +/// ``` +/// +class AvoidHardcodedUnicode extends DartLintRule { + /// Creates a new instance of [AvoidHardcodedUnicode]. + const AvoidHardcodedUnicode({ + this.allowedRangeStart = 0x20, + this.allowedRangeEnd = 0x7E, + }) : super(code: _code); + + final int allowedRangeStart; + final int allowedRangeEnd; + + static const _code = LintCode( + name: 'avoid_hardcoded_unicode', + problemMessage: 'This string contains hardcoded non-ASCII (Unicode) characters.\n' + 'Ensure all user-facing text is properly internationalized.', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + if (isTestFile(resolver.source)) { + return; + } + context.registry.addSimpleStringLiteral((node) { + final stringValue = node.stringValue; + if (stringValue == null) { + return; + } + if (_containsDisallowedUnicode(stringValue)) { + reporter.atNode(node, _code); + } + }); + + context.registry.addStringInterpolation((node) { + final stringValue = node.toSource(); + if (_containsDisallowedUnicode(stringValue)) { + reporter.atNode(node, _code); + } + }); + } + + /// Checks if the string contains any character outside the allowed ASCII range. + bool _containsDisallowedUnicode(String value) { + for (final codeUnit in value.codeUnits) { + if (codeUnit < allowedRangeStart || codeUnit > allowedRangeEnd) { + // Allow common escape sequences (e.g., \n, \t) + if (codeUnit == 0x0A || codeUnit == 0x0D || codeUnit == 0x09) continue; + return true; + } + } + return false; + } +}