diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c16ff..f47928d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## 0.10.0 + +* **Added:** New custom lint rule (`hardcoded_text_in_widget`) to detect hardcoded string literals in widget `build` methods. + * This lint suggests creating ARB labels named with the widget's class name (e.g., `MyWidget_text`). + * The linter is implemented in a separate package `arb_utils_lints` (version synchronized with `arb_utils`) and integrated as a dev dependency. + +## 0.9.0 + +- Updated dependencies: + - dcli: ^7.0.3 + ## 0.8.3 - Version bump diff --git a/analysis_options.yaml b/analysis_options.yaml index 1f4622f..10dc9eb 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,5 +10,7 @@ include: package:lints/recommended.yaml # - camel_case_types analyzer: + plugins: + - custom_lint # exclude: # - path/to/excluded/files/** diff --git a/arb_utils_lints/lib/arb_utils_lints.dart b/arb_utils_lints/lib/arb_utils_lints.dart new file mode 100644 index 0000000..e9e42d0 --- /dev/null +++ b/arb_utils_lints/lib/arb_utils_lints.dart @@ -0,0 +1,12 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; +// Import the rule file we will create next +import 'src/hardcoded_text_in_widget_lint.dart'; + +PluginBase createPlugin() => _ArbUtilsLinter(); + +class _ArbUtilsLinter extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + HardcodedTextInWidgetLint(), + ]; +} diff --git a/arb_utils_lints/lib/src/hardcoded_text_in_widget_lint.dart b/arb_utils_lints/lib/src/hardcoded_text_in_widget_lint.dart new file mode 100644 index 0000000..b589f68 --- /dev/null +++ b/arb_utils_lints/lib/src/hardcoded_text_in_widget_lint.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class HardcodedTextInWidgetLint extends DartLintRule { + HardcodedTextInWidgetLint() : super(code: _code); + + static const _code = LintCode( + name: 'hardcoded_text_in_widget', + problemMessage: 'Avoid hardcoded text in widget build methods. Consider extracting to an ARB label.', + correctionMessage: 'Try creating an ARB label named like YourWidgetName_text.', + errorSeverity: ErrorSeverity.INFO, // Or WARNING + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addStringLiteral((node) { + // Check if it's a simple string literal, not part of interpolation for now + if (node.stringValue == null) return; + if (node.stringValue!.isEmpty) return; // Ignore empty strings + + // Find the enclosing method declaration + MethodDeclaration? method = node.thisOrAncestorOfType(); + if (method == null || method.name.lexeme != 'build') { + return; + } + + // Find the enclosing class declaration + ClassDeclaration? classDecl = method.thisOrAncestorOfType(); + if (classDecl == null) { + return; + } + + // Check if the class is a Widget + // This is a simplified check. A more robust check might involve resolving the type + // and checking against `Widget`, `StatelessWidget`, `StatefulWidget`. + // For now, we'll assume classes ending with 'Widget' or common Flutter widgets. + // This part might need refinement. + final className = classDecl.name.lexeme; + bool isWidget = className.endsWith('Widget') || + className.endsWith('Page') || + className.endsWith('Screen') || + className.endsWith('Dialog') || + className.endsWith('View'); + // Add more common suffixes if needed, or resolve type properly. + + if (!isWidget) { + // A more robust check: + // final classElement = classDecl.declaredElement; + // if (classElement != null) { + // final type = classElement.thisType; + // // Check if type is subtype of Widget - requires more setup with resolver + // } + return; + } + + // Suggest a label name + final suggestedLabel = '${className}_text'; + // Update correction message with the specific name + final specificCorrection = 'Try creating an ARB label named like ${suggestedLabel}.'; + + + reporter.reportErrorForNode( + LintCode( + name: _code.name, + problemMessage: _code.problemMessage, + correctionMessage: specificCorrection, + errorSeverity: _code.errorSeverity, + uniqueName: _code.uniqueName // Ensure uniqueName is passed if using a new LintCode instance + ), + node, + ); + }); + } +} diff --git a/arb_utils_lints/pubspec.yaml b/arb_utils_lints/pubspec.yaml new file mode 100644 index 0000000..54d632c --- /dev/null +++ b/arb_utils_lints/pubspec.yaml @@ -0,0 +1,16 @@ +name: arb_utils_lints +description: Custom lints for arb_utils. +version: 0.10.0 +publish_to: none # It's an internal linter for now + +environment: + sdk: '>=2.19.0 <4.0.0' # Match arb_utils + +dependencies: + analyzer: ^6.0.0 # Or latest compatible + analyzer_plugin: ^0.11.0 # Or latest compatible + custom_lint_builder: ^0.6.0 # Or latest compatible (align with custom_lint version 0.7.5 found earlier) + +dev_dependencies: + lints: ^4.0.0 + test: ^1.25.2 diff --git a/example/test_widget_for_lint.dart b/example/test_widget_for_lint.dart new file mode 100644 index 0000000..ac4fcaa --- /dev/null +++ b/example/test_widget_for_lint.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class MyTestWidget extends StatelessWidget { + const MyTestWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const Text('This is a hardcoded string.'), // Expect lint here + const Text('Another hardcoded string here.'), // Expect lint here + Text('Hello ' + 'World'), // Expect lint for 'Hello World' + const Text(''), // Should be ignored by the lint + Text('Value: \${1 + 2}'), // String interpolation, might be ignored by current simple check, or linted. Let's see. + // The current lint logic `node.stringValue == null` check should ignore this. + ], + ); + } +} + +class NotAWidget { + void build() { + // ignore: unused_local_variable + String test = 'This should not trigger the lint.'; + } +} + +class AnotherTestScreen extends StatefulWidget { + const AnotherTestScreen({super.key}); + + @override + State createState() => _AnotherTestScreenState(); +} + +class _AnotherTestScreenState extends State { + @override + Widget build(BuildContext context) { + return const Text('Stateful widget hardcoded text.'); // Expect lint here + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 5fc292c..32d785a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: arb_utils description: A set of tools to work with .arb files (the preferred Dart way of dealing with i18n/l10n/translations) -version: 0.8.3 +version: 0.10.0 repository: https://github.com/Rodsevich/arb_utils homepage: https://gitlab.com/Rodsevich/arb_utils @@ -15,13 +15,16 @@ dependencies: intl: ">=0.18.0 <0.20.0" intl_utils: ^2.8.7 shared_preferences: ^2.2.2 - dcli: ^6.0.5 + dcli: ^7.0.3 dart_console: ^4.1.0 dev_dependencies: lints: ^4.0.0 test: ^1.25.2 win32: ^5.4.0 + custom_lint: ^0.6.4 + arb_utils_lints: + path: ./arb_utils_lints executables: arb_utils: