Hardcoded Strings Lint
A Flutter analyzer plugin that detects hardcoded strings in widget constructors, encouraging better internationalization and code maintainability.
Overview
hardcoded_strings_lint is built on top of Dart's official analysis_server_plugin system, so its diagnostics surface natively in dart analyze and inside the Dart Analysis Server (VS Code, IntelliJ, etc.) without any extra commands.
Features
Smart Detection
- Flags hardcoded strings passed directly to Flutter widget constructor arguments.
- Skips strings inside callback bodies (e.g.
onTap: () { logger.info('...'); }). - Walks the inheritance chain so custom
Widgetsubclasses are detected.
Intelligent Filtering
- Technical patterns skipped: URLs, emails, hex colors, snake_case, CONSTANT_CASE, dotted notation, file paths.
- Short strings skipped: empty and ≤ 2 characters.
- Map keys skipped:
map['k']and{'k': value}. - Acceptable widget properties skipped:
semanticsLabel,restorationId,heroTag,key,debugLabel,tooltip,fontFamily,package,asset,textDirection,textAlign, and others.
Quick Fixes
- Add ignore comment — inserts the prefixed
// ignore: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgetsabove the offending line. - Ignore for whole file — inserts
// ignore_for_file: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgetsat the top of the file (or appends to an existingignore_for_filecomment). - Ignore in
analysis_options.yaml— disables the rule project-wide by writingdiagnostics: { avoid_hardcoded_strings_in_widgets: false }into youranalysis_options.yaml. - Extract to variable — extracts the literal to a
constlocal (inside a method) or astatic constfield (inside a class), naming the variable from the string contents (e.g.'Hello, World!'→helloWorldText).
Installation
1. Configure analysis_options.yaml
Add the plugin under the top-level plugins: block (not under analyzer:):
plugins:
hardcoded_strings_lint: ^2.1.0
The plugin's diagnostic is enabled by default. To explicitly disable or re-enable it use the nested diagnostics: map:
plugins:
hardcoded_strings_lint:
version: ^2.0.0
diagnostics:
avoid_hardcoded_strings_in_widgets: true
Custom warning messages
Override the lint message and correction hint to use project-specific language (e.g. pointing to your own i18n docs):
plugins:
hardcoded_strings_lint:
options:
message: "Hardcoded string! Externalize to your ARB file."
correction_message: "See docs/i18n.md for instructions."
diagnostics:
avoid_hardcoded_strings_in_widgets: true
Both message and correction_message are optional. Omitting either keeps that field's built-in default. An empty string also falls back to the default.
Silencing the unsupported_option warning
The analyzer validates the plugins: block against a fixed key allowlist — diagnostics, git, path, version, hosted — and offers no extension point for package-specific keys. It therefore reports a warning on the options: block above:
warning - analysis_options.yaml:3:5 - The option 'options' isn't supported by
'plugins/hardcoded_strings_lint'. - unsupported_option
This is a false positive: the plugin reads options: by parsing analysis_options.yaml directly, so your custom messages do take effect. Silence it with:
analyzer:
errors:
unsupported_option: ignore
If you'd rather not add that stanza, omit the options: block and use the built-in default messages.
2. Resolve and analyze
dart pub get
dart analyze
That's it — no separate dart run … command, no dev_dependencies entry, no analyzer.plugins block.
Restart the analyzer in your IDE after first installing the plugin or changing the
plugins:section.
Usage
class WelcomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Welcome to Our App'), // ⚠️ flagged
),
body: Column(
children: [
Text('Hello, World!'), // ⚠️ flagged
ElevatedButton(
onPressed: () {},
child: Text('Get Started'), // ⚠️ flagged
),
],
),
);
}
}
class WelcomeScreen extends StatelessWidget {
static const _welcomeTitle = 'Welcome to Our App';
static const _helloText = 'Hello, World!';
static const _getStartedText = 'Get Started';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(_welcomeTitle), // ✅
backgroundColor: Colors.blue, // ✅ technical value
),
body: Column(
children: [
Text(_helloText), // ✅
ElevatedButton(
onPressed: () {},
child: Text(_getStartedText), // ✅
),
Image.asset('assets/logo.png'), // ✅ asset path
],
),
);
}
}
Ignoring warnings
The plugin uses the analyzer's native ignore-comment system. The hardcoded_strings_lint/ prefix is required — bespoke shorthands like // hardcoded.ok are no longer recognized.
Per line
// ignore: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
Text('This is acceptable hardcoded text'),
Per file
// ignore_for_file: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
class DebugScreen extends StatelessWidget { /* ... */ }
Smart filtering rules
Technical patterns automatically skipped
- URLs:
https://example.com,file://path - Email addresses:
user@example.com - Hex colors:
#FF5722,#ffffff - File paths:
/assets/images/logo.png - snake_case / CONSTANT_CASE identifiers
- Dotted notation:
package.asset
Acceptable widget properties
Text('hi', semanticsLabel: 'A long accessibility label'), // ✅
Scaffold(restorationId: 'home_scaffold'), // ✅
Full allowlist: semanticsLabel, excludeSemantics, restorationId, heroTag, key, debugLabel, fontFamily, package, name, asset, tooltip, textDirection, locale, materialType, clipBehavior, crossAxisAlignment, mainAxisAlignment, textAlign, textBaseline, overflow, softWrap, textScaleFactor.
Quick fixes
Add ignore comment
// Before:
Text('Hello World')
// After:
// ignore: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
Text('Hello World')
Ignore for whole file
// ignore_for_file: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
class DebugScreen extends StatelessWidget { /* ... */ }
If ignore_for_file: is already present in the file the rule name is appended to the existing comment rather than adding a second one.
Ignore in analysis_options.yaml
Disables the rule for the entire project by adding the following to your analysis_options.yaml:
plugins:
hardcoded_strings_lint:
diagnostics:
avoid_hardcoded_strings_in_widgets: false
The fix creates the plugins, hardcoded_strings_lint, and diagnostics keys if any are absent, and is a no-op if the rule is already disabled.
Extract to variable
// Before (inside a build method):
Text('Welcome to our application')
// After:
@override
Widget build(BuildContext context) {
const welcomeToOurText = 'Welcome to our application';
return Text(welcomeToOurText);
}
// Before (inside a class, outside a method):
class MyWidget extends StatelessWidget {
final Widget header = Text('Welcome to our application');
}
// After:
class MyWidget extends StatelessWidget {
static const welcomeToOurText = 'Welcome to our application';
final Widget header = Text(welcomeToOurText);
}
Migrating from 1.x
Version 2.0.0 is a clean break. The package is now built on Dart's official analysis_server_plugin system instead of the deprecated custom_lint_builder ecosystem.
Why the break: custom_lint is no longer actively developed. The Dart team's analysis_server_plugin is the official replacement, ships with Dart 3.10 (Flutter 3.38) and later, and removes the need for a separate dart run custom_lint step.
pubspec.yaml
environment:
- sdk: ^3.6.0
+ sdk: ^3.11.0 # Flutter 3.41+
dev_dependencies:
- custom_lint: ^0.8.1
- analyzer: ^8.1.1
- hardcoded_strings_lint: ^1.0.4
The plugin is no longer a dev_dependency — it's loaded entirely through analysis_options.yaml.
analysis_options.yaml
-analyzer:
- plugins:
- - custom_lint
+plugins:
+ hardcoded_strings_lint: ^2.0.0
Note the plugins: block is now a top-level key, not nested under analyzer:.
Ignore comments
The ignore syntax now requires the hardcoded_strings_lint/ prefix:
-// ignore: avoid_hardcoded_strings_in_widgets
-// ignore: hardcoded.string
-// hardcoded.ok
+// ignore: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
The bespoke // hardcoded.ok and // ignore: hardcoded.string shorthands are removed.
No more dart run custom_lint
dart analyze and the Dart Analysis Server pick up the plugin automatically. There's nothing else to run.
Troubleshooting
Rule not running
Use dart analyze, not flutter analyze. flutter analyze does not run analyzer plugins, so this rule reports nothing under it. The same project that reports 3 hardcoded strings under dart analyze reports 0 under flutter analyze. Your IDE (which talks to the Dart Analysis Server directly) does run the plugin.
If dart analyze also reports nothing:
- Confirm
hardcoded_strings_lintis listed underplugins:inanalysis_options.yaml. - Restart the Dart Analysis Server / your IDE — plugin changes only take effect after a restart.
- Run
dart pub get. - Run
dart analyzeto surface diagnostics from the command line.
Ignore comments not taking effect
This can happen in workspaces with multiple analysis_options.yaml files (see dart-lang/sdk#62173). The fix is tracked upstream; for now, hoist the plugins: block to a single root analysis options file.
False positives
- Verify the string really matches a technical pattern listed above. If not, add an ignore comment or rename the value to fit a pattern (e.g.
snake_case). - Consider whether the widget argument should be added to the acceptable allowlist; PRs welcome.
Best practices
// Recommended: localization
Text(AppLocalizations.of(context).welcomeMessage)
// Or grouped constants
class AppStrings {
static const appTitle = 'My App';
static const welcomeMessage = 'Welcome';
static const loginButton = 'Login';
}
// Use ignore comments sparingly — debug/dev only
// ignore: hardcoded_strings_lint/avoid_hardcoded_strings_in_widgets
Text('DEBUG: state=$state')
License
This package is released under the MIT License. See LICENSE for details.
Changelog
See CHANGELOG.md for version history.