pub package GitHub stars GitHub forks GitHub license GitHub issues flutter-candies

Languages: English | 中文简体


The plugin to help create custom lint quickly.

simple use

add into pubspec.yaml

  # zmtzawqlp  
  candies_analyzer_plugin: any

add into analysis_options.yaml

  # zmtzawqlp  

default lints are following:

  • prefer_asset_const
  • prefer_named_routes
  • prefer_safe_setState
  • must_call_super_dispose
  • must_call_super_dispose
  • perfer_doc_comments
  • prefer_singleton
  • good_doc_comments
  • prefer_trailing_comma

more info please check Default lints(#Default lints)

Custom your analyzer plugin

Create Template

  1. activate plugin

    run dart pub global activate candies_analyzer_plugin

  2. cd to your project

    Let us suppose:

    your project is example

    your lint plugin is custom_lint

    run candies_analyzer_plugin --exmaple custom_lint, a simple lint plugin is generated.

  3. add custom_lint into dev_dependencies of the root pubspec.yaml

  # zmtzawqlp  
    path: custom_lint/
  1. add custom_lint into analyzer plugins of the root analysis_options.yaml
  # zmtzawqlp  

after analysis are finished, you will see some custom lint in your ide.

Add your lint

find plugin.dart base on following project tree

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ plugin.dart

plugin.dart is the entrance of plugin.

start plugin

we start plugin in this file.

CandiesAnalyzerPlugin get plugin => CustomLintPlugin();

// This file must be 'plugin.dart'
void main(List<String> args, SendPort sendPort) {
    plugin: plugin,

class CustomLintPlugin extends CandiesAnalyzerPlugin {
  String get name => 'custom_lint';

  List<String> get fileGlobsToAnalyze => const <String>[

  List<DartLint> get dartLints => <DartLint>[
        // add your dart lint here

  List<YamlLint> get yamlLints => <YamlLint>[RemoveDependency(package: 'path')];

  List<GenericLint> get genericLints => <GenericLint>[RemoveDuplicateValue()];

create a lint

you just need to make a custom lint which extends from DartLint ,YamlLint, GenericLint.


Property Description Default
code The name, as a string, of the error code associated with this error. required
message The message to be displayed for this error. The message should indicate what is wrong with the code and why it is wrong. required
url The URL of a page containing documentation associated with this error.
type The type of the error.
The default is LINT.
severity The severity of the error.
The default is INFO.
correction The correction message to be displayed for this error. The correction message should indicate how the user can fix the error. The field is omitted if there is no correction message associated with the error code.
contextMessages Additional messages associated with this diagnostic that provide context to help the user understand the diagnostic.

Important methodes:

Method Description Override
matchLint return whether is match lint. must
getDartFixes/getYamlFixes/getGenericFixes return fixes if has. getYamlFixes/getGenericFixes doesn't work for now, leave it in case dart team maybe support it someday in the future, see issue

dart lint

you can ignore lint or ignore file by override ignoreLint and ignoreFile. Here is a demo for a dart lint:

class PerferCandiesClassPrefix extends DartLint {
  String get code => 'perfer_candies_class_prefix';

  String? get url => '';

  SyntacticEntity? matchLint(AstNode node) {
    if (node is ClassDeclaration) {
      final String name = node.name2.toString();
      final int startIndex = _getClassNameStartIndex(name);
      if (!name.substring(startIndex).startsWith('Candies')) {
        return node.name2;
    return null;

  String get message => 'Define a class name start with Candies';

  Future<List<SourceChange>> getDartFixes(
    DartAnalysisError error,
    CandiesAnalyzerPluginConfig config,
  ) async {
    final ResolvedUnitResult resolvedUnitResult = error.result;
    final AstNode astNode = error.astNode;
    // get name node
    final Token nameNode = (astNode as ClassDeclaration).name2;
    final String nameString = nameNode.toString();
    return <SourceChange>[
      await getDartFix(
        resolvedUnitResult: resolvedUnitResult,
        message: 'Use Candies as a class prefix.',
        buildDartFileEdit: (DartFileEditBuilder dartFileEditBuilder) {
          final int startIndex = _getClassNameStartIndex(nameString);

          final RegExp regExp = RegExp(nameString);

          final String replace =
              '${nameString.substring(0, startIndex)}Candies${nameString.substring(startIndex)}';

          for (final Match match
              in regExp.allMatches(resolvedUnitResult.content)) {
                SourceRange(match.start, match.end - match.start), replace);


  int _getClassNameStartIndex(String nameString) {
    int index = 0;
    while (nameString[index] == '_') {
      if (index == nameString.length - 1) {
    return index;

yaml lint

Here is a demo for a yaml lint:

class RemoveDependency extends YamlLint {
  RemoveDependency({required this.package});
  final String package;
  String get code => 'remove_${package}_dependency';

  String get message => 'don\'t use $package!';

  String? get correction => 'Remove $package dependency';

  AnalysisErrorSeverity get severity => AnalysisErrorSeverity.WARNING;

  Iterable<SourceRange> matchLint(
    YamlNode root,
    String content,
    LineInfo lineInfo,
  ) sync* {
    if (root is YamlMap && root.containsKey(PubspecField.DEPENDENCIES_FIELD)) {
      final YamlNode dependencies =
      if (dependencies is YamlMap && dependencies.containsKey(package)) {
        final YamlNode get = dependencies.nodes[package]!;
        int start = dependencies.span.start.offset;
        final int end = get.span.start.offset;
        final int index = content.substring(start, end).indexOf('$package: ');
        start += index;
        yield SourceRange(start, get.span.end.offset - start);

generic lint

Here is a demo for a generic lint:

class RemoveDuplicateValue extends GenericLint {
  String get code => 'remove_duplicate_value';

  Iterable<SourceRange> matchLint(
    String content,
    String file,
    LineInfo lineInfo,
  ) sync* {
    if (isFileType(file: file, type: '.json')) {
      final Map<dynamic, dynamic> map =
          jsonDecode(content) as Map<dynamic, dynamic>;

      final Map<dynamic, dynamic> duplicate = <dynamic, dynamic>{};
      final Map<dynamic, dynamic> checkDuplicate = <dynamic, dynamic>{};
      for (final dynamic key in map.keys) {
        final dynamic value = map[key];
        if (checkDuplicate.containsKey(value)) {
          duplicate[key] = value;
          duplicate[checkDuplicate[value]] = value;
        checkDuplicate[value] = key;

      if (duplicate.isNotEmpty) {
        for (final dynamic key in duplicate.keys) {
          final int start = content.indexOf('"$key"');
          final dynamic value = duplicate[key];
          final int end = content.indexOf(
              ) +
              value.toString().length +

          final int lineNumber = lineInfo.getLocation(end).lineNumber;

          bool hasComma = false;
          int commaIndex = end;
          int commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;

          while (!hasComma && commaLineNumber == lineNumber) {
            final String char = content[commaIndex];
            hasComma = char == ',';
            commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;

          yield SourceRange(start, (hasComma ? commaIndex : end) + 1 - start);

  String get message => 'remove duplicate value';


debug lint

find debug.dart base on following project tree

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ debug.dart

change root to which you want to debug, default is example folder.

import 'dart:io';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
import 'package:candies_analyzer_plugin/candies_analyzer_plugin.dart';
import 'plugin.dart';

Future<void> main(List<String> args) async {
  final String root = Directory.current.parent.parent.parent.path;
  final AnalysisContextCollection collection =
      AnalysisContextCollection(includedPaths: <String>[root]);

  final CandiesAnalyzerPlugin myPlugin = plugin;
  for (final AnalysisContext context in collection.contexts) {
    for (final String file in context.contextRoot.analyzedFiles()) {
      if (!myPlugin.shouldAnalyzeFile(file, context)) {

      final bool isAnalyzed = context.contextRoot.isAnalyzed(file);
      if (!isAnalyzed) {

      final List<AnalysisError> errors =
          (await myPlugin.getAnalysisErrorsForDebug(
      for (final AnalysisError error in errors) {
        final List<AnalysisErrorFixes> fixes = await myPlugin
                EditGetFixesParams(file, error.location.offset), context)


update code

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin

you have two options to update new code into dartServer.

  1. delete .plugin_manager folder

Note, analyzer_plugin folder will be copyed into .plugin_manager and create a folder base on encrypt plugin path.

macos: /Users/user_name/.dartServer/.plugin_manager/

windows: C:\Users\user_name\AppData\Local\.dartServer\.plugin_manager\

if your code is changed, please remove the files under .plugin_manager.

or you can run candies_analyzer_plugin --clear-cache to remove the files under .plugin_manager.

  1. write new code under custom_lint folder

you can write new code under custom_lint, for exmaple, in custom_lint.dart.

├─ example
│  ├─ custom_lint
│  │  ├─ lib
│  │  │  └─ custom_lint.dart

so you should add custom_lint dependencies into analyzer_plugin\pubspec.yaml

you should use absolute path due to analyzer_plugin folder will copy to .plugin_manager.

if your don't publish custom_lint as new package, i don't suggest do as this.

├─ example
│  ├─ custom_lint
│  │  ├─ lib
│  │  │  └─ custom_lint.dart
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ analysis_options.yaml
    # absolute path  
    path: xxx/xxx/custom_lint
  candies_analyzer_plugin: any
  path: any
  analyzer: any
  analyzer_plugin: any

restart server

after update code, you should restart analysis server by following steps in vscode.

  1. find Command Palette in View

  1. enter Restart Analysis Server

now, you can see the new change.


  1. for performance, default is false, if you want to check log, set it to true. you can open Log.

    CandiesAnalyzerPluginLogger().shouldLog = true;

Under the project custom_lint.log will be generated.

  1. you can custom log name

    CandiesAnalyzerPluginLogger().logFileName = 'your name';

  2. log info

        // which location custom_lint.log will be generated
        root: result.root,
  1. log error
     'analyze file failed:',
     root: analysisContext.root,
     error: e,
     stackTrace: stackTrace,


disable a lint

As default, all of the custom lints are enable. And you can also write a config in analysis_options.yaml to disable they.

  1. add ignore for a lint.
    perfer_candies_class_prefix: ignore
  1. exclude files
    - lib/exclude/*.dart
  1. disable a lint
    # disable a lint
    perfer_candies_class_prefix: false 


we can define include tag under custom_lint (it's your plugin name). it means that we only analyze the include files.

# your plugin name
  # if we define this, we only analyze include files
    - lib/include/*.dart

custom lint severity

you can change lint severity by following setting.

change the severity of perfer_candies_class_prefix from info to warning.

support warning , info , error.

    # override error severity
    perfer_candies_class_prefix: warning

Default lints


Define a class name start with prefix

class PerferClassPrefix extends DartLint {

  final String prefix;

  String get code => 'perfer_${prefix}_class_prefix';


Prefer to use asset const instead of a string.

class PreferAssetConst extends DartLint {
  String get code => 'prefer_asset_const';
  String? get url => '';  


Prefer to use named routes.

class PreferNamedRoutes extends DartLint {
  String get code => 'prefer_named_routes';
  String? get url => '';  


Prefer to check mounted before setState

class PerferSafeSetState extends DartLint {
  String get code => 'prefer_safe_setState';


Implementations of this method should end with a call to the inherited method, as in super.dispose().

class MustCallSuperDispose extends DartLint with CallSuperDisposeMixin {
  String get code => 'must_call_super_dispose';


Should call super.dispose() at the end of this method.

class EndCallSuperDispose extends DartLint with CallSuperDisposeMixin {
  String get code => 'end_call_super_dispose';

PerferDocComments The same like public_member_api_docs, but we can ignore lint or ignore file by override ignoreLint and ignoreFile and you can override isPrivate and inPrivateMember to check private member.

class PerferDocComments extends DartLint {
  String get code => 'perfer_doc_comments';


This is not a singleton, and new Object every time.

class PreferSingleton extends DartLint {
  String get code => 'prefer_singleton';


wrong comments format. (/// xxx) for public api and (// xxx) for other cases.

class GoodDocComments extends DartLint {
  String get code => 'good_doc_comments';


Prefer trailing comma for better code style.

class PreferTrailingComma extends DartLint {
  String get code => 'prefer_trailing_comma';


Make a custom completion

you can define your CompletionContributor in completionContributors, ExtensionMemberContributor is default.

  /// The completionContributors to finish CompletionRequest
  List<CompletionContributor> get completionContributors =>

Suggestions of Extension Member

Although dart team had close the issue Auto import (or quickfix?) for Extensions · Issue #38894 · dart-lang/sdk ( , but still has many problems when developing in different ide.

ExtensionMemberContributor help to handle extension member easily.



find pre_commit.dart base on following project tree, it's a demo to check errors before submit code.

├─ example
│  ├─ custom_lint
│  │  └─ tools
│  │     └─ analyzer_plugin
│  │        ├─ bin
│  │        │  └─ pre_commit.dart

pre-commit script

run candies_analyzer_plugin --pre-commit, pre-commit script is generated under .git/hooks.

and you can modify this script template by add a pre-commit file under your project.

├─ example
│  ├─ pre-commit

{0} and {1} are placeholder, do not modify them.


# project path

dart format "$base_dir"

# pre_commit.dart path
echo "Checking the code before submit..."
echo "Analyzing $base_dir..."

info=$(dart "$pre_commit" "$base_dir")

echo "$info"

if [[ -n $info && $info != *"No issues found"* ]];then
exit 1

when you are running git commit command to submit code, it will run under .git/hooks/pre-commit script first. and the script will call example/custom_lint/tools/analyzer_plugin/bin/pre_commit.dart, if there are errors, you should exit 1.

you can modify example/pre-commit and example/custom_lint/tools/analyzer_plugin/bin/pre_commit.dart to custom your rule.


set CandiesAnalyzerPlugin.cacheErrorsIntoFile to true, to reduce the spent time to check error before submit code.


don't write print in the process of analyzing in your plugin, analysis will lag.

pubspec.yaml and analysis_options.yaml

you must do following things to support your project to be analyzed.

  1. add custom_lint into dev_dependencies in pubspec.yaml , see pubspec.yaml

  2. add custom_lint into analyzer plugins in analysis_options.yaml see analysis_options.yaml

quick fixes are only supported for dart files in vscode.(android studio support any type of file)


completion auto import is not working in vscode