Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,7 @@ include: package:lints/recommended.yaml
# - camel_case_types

analyzer:
plugins:
- custom_lint
# exclude:
# - path/to/excluded/files/**
12 changes: 12 additions & 0 deletions arb_utils_lints/lib/arb_utils_lints.dart
Original file line number Diff line number Diff line change
@@ -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<LintRule> getLintRules(CustomLintConfigs configs) => [
HardcodedTextInWidgetLint(),
];
}
81 changes: 81 additions & 0 deletions arb_utils_lints/lib/src/hardcoded_text_in_widget_lint.dart
Original file line number Diff line number Diff line change
@@ -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<MethodDeclaration>();
if (method == null || method.name.lexeme != 'build') {
return;
}

// Find the enclosing class declaration
ClassDeclaration? classDecl = method.thisOrAncestorOfType<ClassDeclaration>();
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,
);
});
}
}
16 changes: 16 additions & 0 deletions arb_utils_lints/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions example/test_widget_for_lint.dart
Original file line number Diff line number Diff line change
@@ -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<AnotherTestScreen> createState() => _AnotherTestScreenState();
}

class _AnotherTestScreenState extends State<AnotherTestScreen> {
@override
Widget build(BuildContext context) {
return const Text('Stateful widget hardcoded text.'); // Expect lint here
}
}
7 changes: 5 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Loading