Dart 怎么阻止你的同事使用Getx(自定义Lint)
前言
-
无规则不成方圆,不管什么平台,写什么代码。每一种编程语言都有着自己的语法标准,代码规范,并且在不断更新改进,达到优化语言性能的目的。
-
俗话说
代码不规范,维护两行泪
,说的就是这个道理。我们应该遵守它们,避免代码看起来乱七八糟。
但是如果官方提供的 lint
已经不能满足你的需求,比如,阻止你的同事使用 Getx
,咋办了?不怕,我们可以自定义 lint
。
analyzer_plugin
我们可以通过 analyzer_plugin
来编写属于自己的插件,用于错误分析。
analyzer_plugin 文档,注意,官方的文档,看看就行了,具体问题可以查看 issue。
本文章是基于以下版本
dependencies:
analyzer: ^4.7.0
analyzer_plugin: ^0.11.1
插件结构
比如你想创建插件的名字是 custom_lint
,那么你需要生成如下的项目结构。
├─ custom_lint
│ └─ tools
│ └─ analyzer_plugin
│ ├─ bin
│ │ └─ plugin.dart
入口
入口为 plugin.dart
,
void main(List<String> args, SendPort sendPort) {
// Invoke the real main method in the plugin package.
}
最小的插件
你需要继承 ServerPlugin
,下面几个属性和方法是需要实现的。
这里先简单介绍,后面再细讲。
name
插件的名字version
插件的版本fileGlobsToAnalyze
希望被分析的文件类型analyzeFile
分析文件,做一些操作
class CustomLintPlugin extends ServerPlugin {
CustomLintPlugin()
: super(resourceProvider: PhysicalResourceProvider.INSTANCE);
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
@override
String get name => 'custom_lint';
@override
String get version => '1.0.0';
@override
Future<void> analyzeFile({
required AnalysisContext analysisContext,
required String path,
}) async {}
}
启动插件
void main(List<String> args, SendPort sendPort) {
ServerPluginStarter(
CustomLintPlugin(),
).start(sendPort);
}
将插件加入你的项目
比如我们创建了一个项目叫 example
- 将
custom_lint
增加到 根目录pubspec.yaml
的dev_dependencies
中
dev_dependencies:
custom_lint:
path: 你插件的位置
- 将
custom_lint
增加到根目录analysis_options.yaml
的analyzer plugins
tag 下面
analyzer:
plugins:
custom_lint
这样子我们就做好了做一个插件的前期准备工作了。
增加分析文件代码
过滤文件
在 analyzeFile
方法中,我们需要再过滤下文件,fileGlobsToAnalyze
虽然设置了,但是感觉官方这里会返回跟 dart 相关的一些文件,比如 pubspec.yaml
,analysis_options.yaml
等。
if (!path.endsWith('.dart')) { return; }
获取解析结果
通过 getResolvedUnitResult
方法拿到分析的结果。
final ResolvedUnitResult result = await getResolvedUnitResult(path);
遍历解析结果
我们需要使用一个 AstVisitor
, 来遍历解析结果,找到自己想要的部分。比如说我们这里想做一个限制 Class
名字前缀的 lint
,那么我们需要拿到文件中的 ClassDeclaration
。
class MyAstVisitor extends GeneralizingAstVisitor<void> {
MyAstVisitor(this.classes);
final List<ClassDeclaration> classes;
@override
void visitClassDeclaration(ClassDeclaration node) {
classes.add(node);
super.visitClassDeclaration(node);
}
}
通过 result.unit.visitChildren
方法,遍历获取全部的 ClassDeclaration
final List<ClassDeclaration> classes = <ClassDeclaration>[];
result.unit.visitChildren(MyAstVisitor(classes));
处理结果
如果我们发现 class
的名字不是以 'Candies' 开头的,那么这就是符合我们限制的情况。(这里也处理下以 _
开头的类)
for (final ClassDeclaration classDeclaration in classes) {
final String name = classDeclaration.name2.toString();
final int startIndex = _getClassNameStartIndex(name);
if (!name.substring(startIndex).startsWith('Candies')) {
// TODO
}
}
int _getClassNameStartIndex(String nameString) {
int index = 0;
while (nameString[index] == '_') {
index++;
if (index == nameString.length - 1) {
break;
}
}
return index;
}
生成错误
想让 ide
感知到错误,我们需要生成 AnalysisError
,它包含了错误的一些信息。
属性 | 描述 | 默认 |
---|---|---|
code | 这个错误的名字,唯一. | 必填 |
message | 描述这个错误的信息 | 必填 |
url | 这个错误文档的链接. | |
type | 错误的类型. CHECKED_MODE_COMPILE_TIME_ERRORCOMPILE_TIME_ERRORHINTLINTSTATIC_TYPE_WARNINGSTATIC_WARNINGSYNTACTIC_ERRORTODO | 默认为 LINT. |
severity | 这个错误的严肃性(一般我们修改的是这个).INFOWARNINGERROR | 默认为 INFO. |
correction | 修复这个错误的一些描述. | |
contextMessages | 额外的信息帮助修复这个错误。 |
这里需要使用到 LineInfo
来获取该错误的位置,它是来之于 result
(特别注意下,LineInfo
是针对 ide
来说的,就是说它的行列都是从 1
开始)。
final LineInfo lineInfo = result.lineInfo;
final List<AnalysisError> errors = <AnalysisError>[];
for (final ClassDeclaration classDeclaration in classes) {
final String name = classDeclaration.name2.toString();
final int startIndex = _getClassNameStartIndex(name);
if (!name.substring(startIndex).startsWith('Candies')) {
final CharacterLocation startLocation =
lineInfo.getLocation(classDeclaration.name2.offset);
final CharacterLocation endLocation =
lineInfo.getLocation(classDeclaration.name2.end);
final AnalysisError error = AnalysisError(
AnalysisErrorSeverity.WARNING,
AnalysisErrorType.LINT,
Location(
path,
classDeclaration.name2.offset,
classDeclaration.name2.length,
startLocation.lineNumber,
startLocation.columnNumber,
endLine: endLocation.lineNumber,
endColumn: endLocation.columnNumber,
),
'Define a class name start with Candies',
'perfer_candies_class_prefix',
);
errors.add(error);
}
}
发送错误
最后将错误发送出去,这样子就完成了一个限制Class
名字前缀 lint
。
channel.sendNotification(
AnalysisErrorsParams(path, errors).toNotification(),
);
完整代码
增加了异常处理,完整代码如下。
class CustomLintPlugin extends ServerPlugin {
CustomLintPlugin()
: super(resourceProvider: PhysicalResourceProvider.INSTANCE);
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
@override
String get name => 'custom_lint';
@override
String get version => '1.0.0';
@override
Future<void> analyzeFile({
required AnalysisContext analysisContext,
required String path,
}) async {
if (!path.endsWith('.dart')) {
return;
}
try {
final ResolvedUnitResult result = await getResolvedUnitResult(path);
final LineInfo lineInfo = result.lineInfo;
final List<ClassDeclaration> classes = <ClassDeclaration>[];
result.unit.visitChildren(MyAstVisitor(classes));
final List<AnalysisError> errors = <AnalysisError>[];
for (final ClassDeclaration classDeclaration in classes) {
final String name = classDeclaration.name2.toString();
final int startIndex = _getClassNameStartIndex(name);
if (!name.substring(startIndex).startsWith('Candies')) {
final CharacterLocation startLocation =
lineInfo.getLocation(classDeclaration.name2.offset);
final CharacterLocation endLocation =
lineInfo.getLocation(classDeclaration.name2.end);
final AnalysisError error = AnalysisError(
AnalysisErrorSeverity.WARNING,
AnalysisErrorType.LINT,
Location(
path,
classDeclaration.name2.offset,
classDeclaration.name2.length,
startLocation.lineNumber,
startLocation.columnNumber,
endLine: endLocation.lineNumber,
endColumn: endLocation.columnNumber,
),
'Define a class name start with Candies',
'perfer_candies_class_prefix',
);
errors.add(error);
}
}
channel.sendNotification(
AnalysisErrorsParams(path, errors).toNotification(),
);
} on Exception catch (e, stackTrace) {
channel.sendNotification(
PluginErrorParams(
false,
e.toString(),
stackTrace.toString(),
).toNotification(),
);
}
}
int _getClassNameStartIndex(String nameString) {
int index = 0;
while (nameString[index] == '_') {
index++;
if (index == nameString.length - 1) {
break;
}
}
return index;
}
}
class MyAstVisitor extends GeneralizingAstVisitor<void> {
MyAstVisitor(this.classes);
final List<ClassDeclaration> classes;
@override
void visitClassDeclaration(ClassDeclaration node) {
classes.add(node);
super.visitClassDeclaration(node);
}
}
清除缓存,重启服务
- 删除 .plugin_manager 文件夹,原因后面细讲。
macos: /Users/user_name/.dartServer/.plugin_manager/
windows: C:\Users\user_name\AppData\Local\.dartServer\.plugin_manager\
- 在
View
下面找到Command Palette
- 输入
Restart Analysis Server
现在可以看到更新后的效果。
增加快速修复
我们需要实现 handleEditGetFixes
方法。
@override
Future<EditGetFixesResult> handleEditGetFixes(
EditGetFixesParams parameters) async {
return EditGetFixesResult(const <AnalysisErrorFixes>[]);
}
保存 AnalysisContextCollection
保存 AnalysisContextCollection
,供 handleEditGetFixes
方法中使用。
late AnalysisContextCollection _analysisContextCollection;
@override
Future<void> afterNewContextCollection({
required AnalysisContextCollection contextCollection,
}) async {
_analysisContextCollection = contextCollection;
return super
.afterNewContextCollection(contextCollection: contextCollection);
}
保存错误
由于 ResolvedUnitResult
中的 errors
是不包含自定义的错误的,所以我们需要在 analyzeFile
方法中缓存下对应的错误。
下面代码删掉了无关部分。
final Map<String, Set<AnalysisError>> _errorMap =
<String, Set<AnalysisError>>{};
@override
Future<void> analyzeFile({
required AnalysisContext analysisContext,
required String path,
}) async {
final Set<AnalysisError> errors = <AnalysisError>{};
_errorMap[path] = errors;
channel.sendNotification(
AnalysisErrorsParams(path, errors).toNotification(),
}
获取错误
根据文件路径从缓存的错误中找到这个错误。
@override
Future<EditGetFixesResult> handleEditGetFixes(
EditGetFixesParams parameters) async {
final String file = parameters.file;
if (_errorMap.containsKey(parameters.file)) {
final List<AnalysisError> errors = _errorMap[file]!.toList();
for (final AnalysisError error in errors) {
if (error.code == 'perfer_candies_class_prefix') {}
}
}
return EditGetFixesResult(const <AnalysisErrorFixes>[]);
}
生成快速修复
- 通过保存的
AnalysisContextCollection
创建ChangeBuilder
。 - 通过
AnalysisContextCollection
的currentSession
创建ChangeBuilder
- 然后通过
ChangeBuilder
来生成SourceChange
final String file = parameters.file;
if (_errorMap.containsKey(file)) {
final AnalysisContext context =
_analysisContextCollection.contextFor(file);
final ResolvedUnitResult result = await getResolvedUnitResult(file);
final List<AnalysisError> errors = _errorMap[file]!.toList();
final List<AnalysisErrorFixes> sourceChanges = <AnalysisErrorFixes>[];
for (final AnalysisError error in errors) {
final int start = error.location.offset;
final int end = error.location.offset + error.location.length;
if (error.code == 'perfer_candies_class_prefix' &&
start <= parameters.offset &&
parameters.offset <= end) {
final ChangeBuilder changeBuilder = ChangeBuilder(
session: context.currentSession,
);
final String nameString = result.content.substring(
start,
end,
);
await changeBuilder.addDartFileEdit(
file,
(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(result.content)) {
dartFileEditBuilder.addSimpleReplacement(
SourceRange(match.start, match.end - match.start),
replace,
);
}
//format
dartFileEditBuilder.format(SourceRange(0, result.unit.length));
},
);
final SourceChange sourceChange = changeBuilder.sourceChange;
sourceChange.message = 'Use Candies as a class prefix.';
// 同一个错误可能有多个修复
// priority 按照从大到小排序
sourceChanges.add(AnalysisErrorFixes(
error,
fixes: <PrioritizedSourceChange>[
PrioritizedSourceChange(
0,
sourceChange,
)
],
));
}
}
return EditGetFixesResult(sourceChanges);
}
有意思的是,ChangeBuilder
有以下三个方法,但是对于快速修复来说,并不支持除了 dart
之外的文件,查看 issue.
- addDartFileEdit
- addYamlFileEdit
- addGenericFileEdit
常用方法: 删除,插入,替换。
- addDeletion
- addInsertion
- addSimpleInsertion
- addReplacement
- addSimpleReplacement
例子完整代码
实现一个限制 Class
名字前缀的 lint
,还是比较简单的,不超过 200
行。
import 'dart:isolate';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/source/line_info.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:analyzer_plugin/plugin/plugin.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
import 'package:analyzer_plugin/protocol/protocol_generated.dart';
import 'package:analyzer_plugin/starter.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_dart.dart';
void main(List<String> args, SendPort sendPort) {
ServerPluginStarter(
CustomLintPlugin(),
).start(sendPort);
}
class CustomLintPlugin extends ServerPlugin {
CustomLintPlugin()
: super(resourceProvider: PhysicalResourceProvider.INSTANCE);
@override
List<String> get fileGlobsToAnalyze => <String>['**/*.dart'];
@override
String get name => 'custom_lint';
@override
String get version => '1.0.0';
final Map<String, List<AnalysisError>> _errorMap =
<String, List<AnalysisError>>{};
@override
Future<void> analyzeFile({
required AnalysisContext analysisContext,
required String path,
}) async {
if (!path.endsWith('.dart')) {
return;
}
try {
final ResolvedUnitResult result = await getResolvedUnitResult(path);
final Set<ClassDeclaration> classes = <ClassDeclaration>{};
result.unit.visitChildren(MyAstVisitor(classes));
final LineInfo lineInfo = result.lineInfo;
final List<AnalysisError> errors = <AnalysisError>[];
for (final ClassDeclaration classDeclaration in classes) {
final String name = classDeclaration.name2.toString();
final int startIndex = _getClassNameStartIndex(name);
if (!name.substring(startIndex).startsWith('Candies')) {
final CharacterLocation startLocation =
lineInfo.getLocation(classDeclaration.name2.offset);
final CharacterLocation endLocation =
lineInfo.getLocation(classDeclaration.name2.end);
final AnalysisError error = AnalysisError(
AnalysisErrorSeverity.WARNING,
AnalysisErrorType.LINT,
Location(
path,
classDeclaration.name2.offset,
classDeclaration.name2.length,
startLocation.lineNumber,
startLocation.columnNumber,
endLine: endLocation.lineNumber,
endColumn: endLocation.columnNumber,
),
'Define a class name start with Candies',
'perfer_candies_class_prefix',
);
errors.add(error);
}
}
_errorMap[path] = errors;
channel.sendNotification(
AnalysisErrorsParams(path, errors).toNotification(),
);
} on Exception catch (e, stackTrace) {
channel.sendNotification(
PluginErrorParams(
false,
e.toString(),
stackTrace.toString(),
).toNotification(),
);
}
}
int _getClassNameStartIndex(String nameString) {
int index = 0;
while (nameString[index] == '_') {
index++;
if (index == nameString.length - 1) {
break;
}
}
return index;
}
@override
Future<EditGetFixesResult> handleEditGetFixes(
EditGetFixesParams parameters) async {
try {
final String file = parameters.file;
if (_errorMap.containsKey(file)) {
final AnalysisContext context =
_analysisContextCollection.contextFor(file);
final ResolvedUnitResult result = await getResolvedUnitResult(file);
final List<AnalysisError> errors = _errorMap[file]!.toList();
final List<AnalysisErrorFixes> sourceChanges = <AnalysisErrorFixes>[];
for (final AnalysisError error in errors) {
final int start = error.location.offset;
final int end = error.location.offset + error.location.length;
if (error.code == 'perfer_candies_class_prefix' &&
start <= parameters.offset &&
parameters.offset <= end) {
final ChangeBuilder changeBuilder = ChangeBuilder(
session: context.currentSession,
);
final String nameString = result.content.substring(
start,
end,
);
await changeBuilder.addDartFileEdit(
file,
(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(result.content)) {
dartFileEditBuilder.addSimpleReplacement(
SourceRange(match.start, match.end - match.start),
replace,
);
}
//format
dartFileEditBuilder.format(SourceRange(0, result.unit.length));
},
);
final SourceChange sourceChange = changeBuilder.sourceChange;
sourceChange.message = 'Use Candies as a class prefix.';
// 同一个错误可能有多个修复
// priority 按照从大到小排序
sourceChanges.add(AnalysisErrorFixes(
error,
fixes: <PrioritizedSourceChange>[
PrioritizedSourceChange(
0,
sourceChange,
)
],
));
}
}
return EditGetFixesResult(sourceChanges);
}
} on Exception catch (e, stackTrace) {
channel.sendNotification(
PluginErrorParams(false, e.toString(), stackTrace.toString())
.toNotification(),
);
}
return EditGetFixesResult(const <AnalysisErrorFixes>[]);
}
late AnalysisContextCollection _analysisContextCollection;
@override
Future<void> afterNewContextCollection({
required AnalysisContextCollection contextCollection,
}) async {
_analysisContextCollection = contextCollection;
return super
.afterNewContextCollection(contextCollection: contextCollection);
}
}
class MyAstVisitor extends GeneralizingAstVisitor<void> {
MyAstVisitor(this.classes);
final Set<ClassDeclaration> classes;
@override
void visitClassDeclaration(ClassDeclaration node) {
classes.add(node);
super.visitClassDeclaration(node);
}
}
吐槽一下
面向源码开发
说实话,官方的文档,真的该更新下了,反正我看了好几遍,依然是很懵逼的。但是没关系,只要有源码,你就找到办法。
dart-lang/sdk: The Dart SDK, including the VM, dart2js, core libraries, and more. (github.com)
比如说,我们上面的代码还差 ignore
,那就是 ignore_for_this_file
和 ignore_for_this_line
. 我们需要新增关于 ignore
的 2
个快速修复。以及我们对 ignore
的判断。
那么我们可以在源码里面搜索 class ignore
,你会发现 IgnoreInfo
, 在源码中可以发现,官方是通过用 ignore:
和ignore_for_file:
对文件内容做正则,获取到哪些错误包含了 ignore
,这些 ignore
的位置。很容易猜到,以下规则:
ignore_for_file:
后面包含的错误code
,不用再解析。- 如果某一行符合你定义的
lint
,那么查看它的上一行是否包含ignore:
,并且包含这个lint
的code
对于快速修复,也很简单。
- 如果文件本身没有
ignore_for_file:
,那么直接在首行插入ignore_for_file: code
;如果本身就有,那么找到位置,在它之后添加上该lint
的code
。 - 如果该错误上一行没有
ignore:
,那么直接在上一行插入ignore: code
;如果本身就有,那么找到位置,在它之后添加上该lint
的code
。
很多代码都可以从官方的代码里面扣出来(baipiao),如果有文档那就更好了。
调试
sdk/debugging.md at master · dart-lang/sdk (github.com)
官方提供的调试方式,怎么说了,我想要的是这些?对于我来说,肯定是想要调试代码,查看当时的情况嘛。最后呢,我只能找到个折中的方式,后面细讲。
LineInfo
这个东西把我坑惨了,在做 ignore
的快速修复的时候,大家要注意它返回的行数和列数是从 1
开始的,但是它的 getOffsetOfLine
方法却是从 0
开始的。所以说,在这个体系下,如果你用 LineInfo
拿到的行列数,去使用 getOffsetOfLine
返回这一行的第一个字符的时候,记得减去 1
。
另外,getOffsetOfLine
方法返回的第一个字符,是包括空格的,就是说其实不是你想要的,你眼睛能看到第一个字符。所以你得向后找,第一个 trim
不等于空的,才是你想要的。
实验性
按照官方的说法,尽管我吐槽这么多,但是确实是能用。对于一个团队,能自定义一些 lint
来约束,还是蛮好的。
Note: The plugin support is not currently available for general use.
because the plugin support is experimental
the documentation is preliminary
写到这里都几千字了,那么有没有那种简单快速就能自定义 lint
的方法呢?答案是肯定。
CandiesAnalyzerPlugin
candies_analyzer_plugin | Dart Package (flutter-io.cn)
是一个帮助快速创建自定义 lint
的插件。你可以直接通过命令就可以创建出一个插件的模板,并且写很少的代码就能自定义一个 lint
。
模版创建
-
激活插件
执行命令
dart pub global activate candies_analyzer_plugin
-
到你的项目的根目录
假设:
你的项目叫做
example
你想创建的插件叫做
custom_lint
执行命令
candies_analyzer_plugin custom_lint
, 一个简单插件模板创建成功. -
将
custom_lint
增加到 根目录pubspec.yaml
的dev_dependencies
中
dev_dependencies:
# zmtzawqlp
custom_lint:
path: custom_lint/
- 将
custom_lint
增加到根目录analysis_options.yaml
的analyzer plugins
tag 下面
analyzer:
# zmtzawqlp
plugins:
custom_lint
当分析结束的时候,在你的 ide
中就可以看到一些自定义的 lint
。
后面的步骤中的代码,都在创建的模版当中,你可以通过它,学习怎么自定义 lint
。
增添你的 lint
在下面的项目结构下面找到 plugin.dart
├─ example
│ ├─ custom_lint
│ │ └─ tools
│ │ └─ analyzer_plugin
│ │ ├─ bin
│ │ │ └─ plugin.dart
plugin.dart
是整个插件的入口。
启动插件
我们将在 main 方法中启动我们的插件.
CandiesLintsPlugin get plugin => CustomLintPlugin();
// This file must be 'plugin.dart'
void main(List<String> args, SendPort sendPort) {
CandiesLintsStarter.start(
args,
sendPort,
plugin: plugin,
);
}
class CustomLintPlugin extends CandiesLintsPlugin {
@override
String get name => 'custom_lint';
@override
List<String> get fileGlobsToAnalyze => const <String>[
'**/*.dart',
'**/*.yaml',
'**/*.json',
];
@override
List<DartLint> get dartLints => <DartLint>[
// add your dart lint here
PerferCandiesClassPrefix(),
...super.dartLints,
];
@override
List<YamlLint> get yamlLints => <YamlLint>[RemoveDependency(package: 'path')];
@override
List<GenericLint> get genericLints => <GenericLint>[RemoveDuplicateValue()];
}
创建一个 lint
在模板代码中,展示了如何创建一个关于 dart
,yaml
,其他文件
的 lint
例子。
你只需要创一个新的类来继承 DartLint
,YamlLint
, GenericLint
即可。
属性:
属性 | 描述 | 默认 |
---|---|---|
code | 这个错误的名字,唯一. | 必填 |
message | 描述这个错误的信息 | 必填 |
url | 这个错误文档的链接. | |
type | 在IDE中错误的类型. CHECKED_MODE_COMPILE_TIME_ERRORCOMPILE_TIME_ERRORHINTLINTSTATIC_TYPE_WARNINGSTATIC_WARNINGSYNTACTIC_ERRORTODO | 默认为 LINT. |
severity | 这个错误的严肃性(一般我们修改的是这个).INFOWARNINGERROR | 默认为 INFO. |
correction | 修复这个错误的一些描述. | |
contextMessages | 额外的信息帮助修复这个错误。 |
重要的方法:
方法 | 描述 | 重载 |
---|---|---|
matchLint | 判断是否是你定义的lint | 必须 |
getDartFixes/getYamlFixes/getGenericFixes | 返回快速修复 | getYamlFixes/getGenericFixes 没有效果,保留它以备 dart team 未来某天支持, 查看 issue |
dart lint
下面是一个 dart lint
的例子:
class PerferCandiesClassPrefix extends DartLint {
@override
String get code => 'perfer_candies_class_prefix';
@override
String? get url => 'https://github.com/fluttercandies/candies_analyzer_plugin';
@override
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;
}
@override
String get message => 'Define a class name start with Candies';
@override
Future<List<SourceChange>> getDartFixes(
ResolvedUnitResult resolvedUnitResult,
AstNode astNode,
) async {
// 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)) {
dartFileEditBuilder.addSimpleReplacement(
SourceRange(match.start, match.end - match.start), replace);
}
dartFileEditBuilder.formatAll(resolvedUnitResult.unit);
},
)
];
}
int _getClassNameStartIndex(String nameString) {
int index = 0;
while (nameString[index] == '_') {
index++;
if (index == nameString.length - 1) {
break;
}
}
return index;
}
}
yaml lint
下面是一个 yaml lint
的例子:
class RemoveDependency extends YamlLint {
RemoveDependency({required this.package});
final String package;
@override
String get code => 'remove_${package}_dependency';
@override
String get message => 'don\'t use $package!';
@override
String? get correction => 'Remove $package dependency';
@override
AnalysisErrorSeverity get severity => AnalysisErrorSeverity.WARNING;
@override
Iterable<SourceRange> matchLint(
YamlNode root,
String content,
LineInfo lineInfo,
) sync* {
if (root is YamlMap && root.containsKey(PubspecField.DEPENDENCIES_FIELD)) {
final YamlNode dependencies =
root.nodes[PubspecField.DEPENDENCIES_FIELD]!;
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
下面是一个 generic lint 的例子:
class RemoveDuplicateValue extends GenericLint {
@override
String get code => 'remove_duplicate_value';
@override
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"',
start,
) +
value.toString().length +
1;
final int lineNumber = lineInfo.getLocation(end).lineNumber;
bool hasComma = false;
int commaIndex = end;
int commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;
while (!hasComma && commaLineNumber == lineNumber) {
commaIndex++;
final String char = content[commaIndex];
hasComma = char == ',';
commaLineNumber = lineInfo.getLocation(commaIndex).lineNumber;
}
yield SourceRange(start, (hasComma ? commaIndex : end) + 1 - start);
}
}
}
}
@override
String get message => 'remove duplicate value';
}
调试
调试错误
在模板项目的结构下面找到 debug.dart
,已经自动为你创建了 debug 的例子。你可以通过调试来编写符合你条件的 lint
├─ example
│ ├─ custom_lint
│ │ └─ tools
│ │ └─ analyzer_plugin
│ │ ├─ bin
│ │ │ └─ debug.dart
把 root
修改为你想要调试的项目路径, 默认为 example
的根目录
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 CandiesLintsPlugin myPlugin = plugin;
for (final AnalysisContext context in collection.contexts) {
for (final String file in context.contextRoot.analyzedFiles()) {
if (!myPlugin.shouldAnalyzeFile(file, context)) {
continue;
}
final bool isAnalyzed = context.contextRoot.isAnalyzed(file);
if (!isAnalyzed) {
continue;
}
final List<AnalysisError> errors =
(await myPlugin.getAnalysisErrorsForDebug(
file,
context,
))
.toList();
for (final AnalysisError error in errors) {
final List<AnalysisErrorFixes> fixes = await myPlugin
.getAnalysisErrorFixesForDebug(
EditGetFixesParams(file, error.location.offset), context)
.toList();
print(fixes.length);
}
print(errors.length);
}
}
}
更新代码
├─ example
│ ├─ custom_lint
│ │ └─ tools
│ │ └─ analyzer_plugin
你有2种方式更新代码。
- 删除 .plugin_manager 文件夹
注意, analyzer_plugin
文件夹下面的东西会复制到 .plugin_manager
下面,根据插件的路径加密生成对应的文件夹。
macos: /Users/user_name/.dartServer/.plugin_manager/
windows: C:\Users\user_name\AppData\Local\.dartServer\.plugin_manager\
如果你的代码改变了, 请删除掉 .plugin_manager
下面的文件
或者通过执行 candies_analyzer_plugin clear_cache
来删除 .plugin_manager
下面的文件.
- 把新的代码写到
custom_lint
下面
你可以把新代码写到 custom_lint
下面, 比如在 custom_lint.dart.
├─ example
│ ├─ custom_lint
│ │ ├─ lib
│ │ │ └─ custom_lint.dart
如果这样的话,你必须增加 custom_lint
引用到 analyzer_plugin\pubspec.yaml
当中
你必须使用 绝对路径
,因为 analyzer_plugin
文件夹是会被复制到 .plugin_manager
下面的.
如果你不是要发布一个新的 package 的话,我不建议你使用第2种方式。
├─ example
│ ├─ custom_lint
│ │ ├─ lib
│ │ │ └─ custom_lint.dart
│ │ └─ tools
│ │ └─ analyzer_plugin
│ │ ├─ analysis_options.yaml
dependencies:
custom_lint:
# absolute path
path: xxx/xxx/custom_lint
candies_analyzer_plugin: any
path: any
analyzer: any
analyzer_plugin: any
重启 dart analysis 服务
更新完毕代码之后,你可以通过在 vscode
中,通过下面的方式重启服务。
- 在
View
下面找到Command Palette
- 输入
Restart Analysis Server
分析结束之后,你可以看到最新的结果.
Log
在被分析的项目根目录会生成 custom_lint.log
,用于查看分析过程的信息。
-
为了性能,默认是关闭的,你可以打开.
CandiesAnalyzerPluginLogger().shouldLog = true;
-
你可以更改日志的名字
CandiesAnalyzerPluginLogger().logFileName = 'your name';
-
记录信息
CandiesAnalyzerPluginLogger().log(
'info',
// which location custom_lint.log will be generated
root: result.root,
);
- 记录错误
CandiesAnalyzerPluginLogger().logError(
'analyze file failed:',
root: analysisContext.root,
error: e,
stackTrace: stackTrace,
);
配置
禁止一个 lint
编写的自定义 lints
默认是全部开启的。当然你可以通过在 analysis_options.yaml
增加配置来禁用它。
- 使用
ignore
tag 来禁用.
analyzer:
errors:
perfer_candies_class_prefix: ignore
- 使用
exclude
来过滤掉不想分析的文件
analyzer:
exclude:
- lib/exclude/*.dart
- 通过将某个lint 设置为 false
linter:
rules:
# disable a lint
perfer_candies_class_prefix: false
包含文件
我们可以通过在 custom_lint
(你定义的插件名字) 下面的 include
标记下面增加包含的文件。
如果我们做了这个设置,那么我们就只会分析这些文件。
# your plugin name
custom_lint:
# if we define this, we only analyze include files
include:
- lib/include/*.dart
自定义 lint 严肃性
你可以设置某个 lint
的严肃性。
比如 perfer_candies_class_prefix
把它的严肃性从 info
改为 warning
.
支持 warning
, info
, error
.
analyzer:
errors:
# override error severity
perfer_candies_class_prefix: warning
Default lints
PerferClassPrefix
全部的类已某个前缀开始,这在一个团队指定类名规则的时候是非常有用的。
class PerferClassPrefix extends DartLint {
PerferClassPrefix(this.prefix);
final String prefix;
@override
String get code => 'perfer_${prefix}_class_prefix';
}
PreferAssetConst
asset
资源使用不要直接写字符串,而应该使用定义好的 const
,assets_generator | Dart Package (flutter-io.cn),摊牌不装了,就是打广告。
class PreferAssetConst extends DartLint {
@override
String get code => 'prefer_asset_const';
@override
String? get url => 'https://pub.flutter-io.cn/packages/assets_generator';
}
PreferNamedRoutes
推荐使用命名路由,ff_annotation_route | Dart Package (flutter-io.cn),摊牌不装了,就是打广告。
class PreferNamedRoutes extends DartLint {
@override
String get code => 'prefer_named_routes';
@override
String? get url => 'https://pub.flutter-io.cn/packages/ff_annotation_route';
}
PerferSafeSetState
在使用 setState
之前请先检查 mounted
,特别是有童鞋在异步操作中调用 setState
的时候,记得检查 mounted
。
class PerferSafeSetState extends DartLint {
@override
String get code => 'prefer_safe_setState';
}
MustCallSuperDispose
此方法的实现应以调用继承的方法结束
class MustCallSuperDispose extends DartLint with CallSuperDisposeMixin {
@override
String get code => 'must_call_super_dispose';
}
EndCallSuperDispose
应该在方法的最后才调用 super.dispose()
class EndCallSuperDispose extends DartLint with CallSuperDisposeMixin {
@override
String get code => 'end_call_super_dispose';
}
注意事项
print lag
不要在插件的分析代码中使用 print
,这会导致 analysis
卡顿, 不知道是不是 bug
,反正挺坑的。
怎么让插件生效
只有当你在 pubspec.yaml
和 analysis_options.yaml
中添加了 custom_lint
,分析才会进行,才会有快速修复。
-
将
custom_lint
添加到pubspec.yaml
中的dev_dependencies
, 查看 pubspec.yaml -
将
custom_lint
添加到analysis_options.yaml
中的analyzer
plugins
,查看 analysis_options.yaml
在 vscode 中快速修复只支持 dart 文件.(android studio支持任何文件)
提示自动导入在 vscode 无法完成
结语
终于到结语了,总的来说,能自定义 lint
,对于人多的团队,还是很有必要的。大家在实际工作中,是否也想过做一些代码上的限制呢? 分享下你的经历或者 pr
个有趣的 lint
,马老师已经在群里等着跟你对线了!把 lint
等级调成 error
, 让你的同事写了代码都运行不起来才是王道!不听话的,统统让大宝打一顿!
爱 Flutter
,爱糖果
,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果QQ群:181398081
最最后放上 Flutter Candies 全家桶,真香。
转载自:https://juejin.cn/post/7160482420616069150