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 Widget subclasses 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_widgets above the offending line.
  • Extract to variable — extracts the literal to a const local (inside a method) or a static const field (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.0.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

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')

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

  1. Confirm hardcoded_strings_lint is listed under plugins: in analysis_options.yaml.
  2. Restart the Dart Analysis Server / your IDE — plugin changes only take effect after a restart.
  3. Run dart pub get.
  4. Run dart analyze to 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.

Libraries

main