-
Notifications
You must be signed in to change notification settings - Fork 73
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Consider simplifying package usage #54
Comments
Hello! I'm not against such a project. But it likely shouldn't be within custom_lint instead. It probably would be in a separate package (possibly official). For now, I'd like to focus on nailing the user experience/performance. With the current mechanism, you should be able to kind of do whatever you want. |
Hi. That was my guess as to what your answer would be, but if I may add some more arguments to the contrary... My understanding of the goal of the I don't think a framework from a separate package would be adopted remotely as much as if it was from this package. Additionally, it's better to change it sooner than later to reduce user migration efforts. A package with great performance and functionality misses out on potential if it's not easy to use. After all, this is the problem with It's up to you. If you are still in favor of working on other things, wouldn't mind at all if you want to close this issue. I wrote this code for my own use and thought it wouldn't hurt to share. |
Hence why I mentioned that the side package could be official. I understand your view. I just don't think that the value is high enough to justify doing this over other things (especially when considering I work on many other packages). I believe the current We can most likely go further, yes. But there are some much better things to do, like working on an IDE extension to give superpowers to custom_Lint. |
Alright, makes sense; that's some cool stuff! Thanks for the consideration and for making such awesome packages. |
For reference, I personally use some utilities that allow a For example I typically do: class RiverpodLint extends PluginBase {
@override
Stream<Lint> getLints(ResolvedUnitResult unit) async* {
final visitor = RiverpodVisitor(unit);
yield* unit.unit.accept(visitor) ?? const Stream<Lint>.empty();
}
}
class RiverpodVisitor extends CombiningRecursiveVisitor<Lint> {
@override
Iterable<Lint>? visitExpression(Expression node) sync* {
if (something) {
yield Lint(...);
}
}
} |
The benefit of using streams is that this both:
And in the future, custom_lint could support canceling the previous getLints stream if the file is changed while |
I don't disagree with the value of a stream. I'm not suggesting to remove it and my code above still has a stream too. With your CombiningRecursiveVisitor example, how easy is to have more than one lint rule? How about 10, or 50? Would they share a visitor, in which case there's a lot of code in one place? Would you use multiple visitors, which is unoptimized because each one will visit the entire AST? How easy is it to temporarily remove a lint? With my code I shared, none of this is a problem because the plugin explicitly deals with a list of lint rules. Would you be able to define your rules codes (riverpod_final_providers), their severity, and their message in one place? What if your rule is interested in multiple node types and thus has several If someone else created a lint, how much effort would it take to add it to your own plugin base? With there being no framework to defining lints with this package, that other person may write it substantially different and it's not compatible with your own implementation. Sure, a CombiningRecursiveVisitor could be one way of doing this. So is my version. There's lots of ways. When a developer uses your package for the first time, do you really want to make them make this decision for themselves before they can start writing lints? Any code they create they would need to test as well, assuming they follow that standard. My code version allows somebody to start writing lints immediately, and their lint rule logic is the only code to test. Lastly, at the moment, there aren't very many examples of using your package. The final providers in the example folder was good start for the basics, but doesn't show you how to do anything more than check the features of a top level element. My code uses the exact same classes as Dart's lints (LintRule, NodeLintRegistry, SimpleAstVisitor, edit: LinterVisitor too) and they are written the same way too. This means every single dart lint that developers are familiar with can now be an example of writing your own lint. In fact, you can even copy-paste them into your custom lint plugin, assuming they don't import any dart linter internal code. |
I'll look more into how the SDK writes its lint. If I like it, forking their approach could be reasonable. |
Honestly, I'm not convinced that the SDK approach is inherently faster. Technically having multiple visitors shouldn't be an issue. The amount of iteration done is the same (since the SDK does a loop on all lints on all methods of the visitor) But I can see a value elsewhere. #50 is an added bonus I'll see what I can do. |
Cool, thanks for looking into it. Regardless of the implementation of the visitors, each lint rule will indeed run the same amount of code (inspecting an element to see if it breaks the rule). The increase in cost of multiple visitors is in the visiting itself. Lets say we have 10 lint rules, and our project has a single file with 100 AST nodes within it. If you use a single visitor, and each rule registers its processing with a NodeLintRegistry, then the shared visitor will visit each of the 100 nodes a single time. If each rule creates it's own visitor (which on it's own is This is assuming, though, that the visitors are fully recursive. The shared visitor needs to be, since it doesn't know what its rules are interested in. An individual visitor could possibly visit a smaller portion of elements, but in practice most rules can't and don't do this optimization, because they are interested in lower-level nodes that can be anywhere. It is misleading that each rule (in my example and in the SDK) creates a "_Visitor" class, but this class is never |
Coming back to this as I'm working on designing the new API right now (to support configuration/disabling rules) What do you think about an API similar to: // entrypoint of the plugin
PluginBase createPlugin() => RiverpodAnalysisPlugin();
class RiverpodAnalysisPlugin extends PluginBase {
List<LintRule> getLintRules() => [
PreferFinalProvider(),
...
];
List<Assist> getAssists() => [
ConvertToConsumerWidget(),
ConvertToConsumerStatefulWidget(),
...
];
}
class PreferFinalProvider extends DartLintRule {
// Get the lints for a dart file
Stream<Lint> run(ResolvedUnitResult unit, LintOptions lintOptions) {
// "run" wouldn't be invoked at all if the lint rule is disabled in the analysis_options.yaml or by ignore_for_file
// LintOptions would include a JSON map of options resolved from the analysis_options file.
// TODO return some lints. I have yet to work on potentially changing this part
// We could fork the ErrorReporter class from the SDK.
// Same goes for the NodeLintRegistry logic & other visitor utils if we want them.
}
<possibly move Lint.getFixes to LintRule.getFixes>
}
class ConvertToConsumerWidget extends Assist {
Stream<Assist> run(ResolvedUnitResult unit, AssistOptions assistOptions, {required int offset, required int length}) {
// Same thing but for refactorings
}
} This seems like a significant organization improvement, on top of natively supporting disabling rules and #50 Would that satisfy your simplification needs? About visitors It's more of a performance issue than a simplification issue (when this issue is about simplicity) but: for (final a in listA) {
for (final b in listB) {
a.doSomething(b);
}
} to: for (final b in listB) {
for (final a in listA) {
a.doSomething(b);
}
} If it was simple to merge the visitors, I'd do it no questions asked. But it seems to come with a certain complexity because we now need to register every visitX functions we use. And if someone truly cares about this small performance gain (if it exists), there's an alternate path: not use RecursiveVisitror and only visit the branches that you care about instead of visiting the whole tree. Is that fair? |
By the way, I want to thank you for the time you spent on this issue. It's rare that someone spends as much time & effort as you did. So thank you again for that :) |
No problem, I'm happy to contribute! This seems like a great improvement so far, especially since it lets the plugin know what rules there are ahead of time as you've mentioned.
A change here makes sense to me; then you don't have to refer to the same I really like the way the SDK does this. Their "Fixes" are generic classes that are agnostic to which rule they are used by. Then they provide a map of each rule and the fixes relevant to them. For example, the AddAsync fix can add
Yep, agree with you there! Perhaps instead of a single
I can't say I understand the concern here yet.
Like I had said before, it's rather rare to be able to avoid full recursion. If you have a lint interested in AdjacentStrings, they can be anywhere in the code and so it needs to be recursive. It is like this for pretty much everything but top level elements. Again, great progress! |
How would this be any different from having the LintRule class be/return an AstVisitor?
My concern is in not increasing the complexity for users, for what I currently believe to be a minor performance improvement (possibly even debatable)
custom_lint needs to know when a plugin has finished emitting lints. That matters for things like the command line |
I suppose you're right; it's the same is if you
Maybe I'll try to come up with a more concrete example showing the improvement. In any case, this is a separate issue for the most part and we can ignore it for now.
I don't understand how this could get broken with different code organization. The plugin is done emitting lints when the Stream is closed. That won't and can't change. |
I can try this now... Firstly, to make sure we're on the same page about what sharing a visitor means... In either case, we have our predefined lint rule list, which can be any size: final rules = <LintRule>[
// lint rule #1
// lint rule #2
// ...
]; Sharing a visitor // Create a single visitor, which has each rule register in its NodeLintRegistry
final registry = NodeLintRegistry(false);
final visitor = LinterVisitor(registry);
for (final rule in rules) {
// Registry done here:
rule.registerNodeVisitors(registry, resolvedUnitResult);
}
// Visit the AST tree. The visitor recurs over every element once. Because it's a [LinterVisitor],
// it calls each registered callback from each rule for each node it finds.
resolvedUnitResult.unit.visitChildren(visitor); Individual visitors: for (final rule in rules) {
rule.doYourLintingByYourself(resolvedUnitResult);
}
// With the LintRule class:
void doYourLintingByYourself(ResolvedUnitResult resolvedUnitResult) {
resolvedUnitResult.unit.visitChildren(someVisitorForThisRule);
} The first approach adds some overhead from registering callbacks in the registry, and them calling them. The registry works using lists, so callbacks are only attempted to be called if they have been created. This means this overhead has a time complexity of O(2c) where c is the number of registered callbacks. I know, officially, that would be simplified to O(c), but I want to avoid simplifying. This is a very minor overhead since the number of callbacks is pretty small. No matter the approach, the same amount of time will be spent for each rule to do it's computing (whether a node breaks its rules). We will call the number of rules r. We have a time complexity of O(r). If we share a visitor, the recursive AST visiting will happen once, so each node is visited once. If the number of nodes in the code is n, we have a time complexity of O(n). If we use different visitors, each will recursively visit the tree on its own -- it will look at each element and it's children. This is a time complexity of O(rn). Since it looks like the letter m when written together :P I'll clarify that's r (# of rules) times n (# of nodes). This is ignoring possible optimizations of a rule being able to visit a smaller part of the tree, but as I've mentioned before, this optimization is unlikely to happen for most rules. Summary. Shared visitor: O(n + r + 2c) "r * n" is significantly expensive, adding much more work per lint rule added, especially since n is expected to be a very large number. This is my first time using big O notation outside of college, so it's possible I used it entirely wrong lol. |
Thanks again for the time spent on this! My concern is that the cost of every But anyway, considering all the time you took to promote this approach, I figured I might as well write a benchmark. Shared visitor benchmark: // Import BenchmarkBase class.
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:benchmark_harness/benchmark_harness.dart';
void main() {
SharedVisitorBenchmark().report();
}
// Create a new benchmark by extending BenchmarkBase
class SharedVisitorBenchmark extends BenchmarkBase {
SharedVisitorBenchmark() : super('Shared visitors');
late ParseStringResult result;
late NodeLintRegistry registry;
// The benchmark code.
@override
void run() {
result.unit.accept(SharedVisitor(registry));
}
// Not measured setup code executed prior to the benchmark runs.
@override
void setup() {
registry = NodeLintRegistry(false);
registry.addMethodDeclaration(
Linter1(
name: 'linter1',
group: Group('name'),
description: '',
details: 'details',
),
Linter1Impl(),
);
registry.addExpressionStatement(
Linter2(
name: 'linter1',
group: Group('name'),
description: '',
details: 'details',
),
Linter2Impl(),
);
result = parseFile(
path:
'/Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/lib/custom_lint.dart',
featureSet: FeatureSet.latestLanguageVersion(),
);
}
}
class SharedVisitor extends LinterVisitor {
SharedVisitor(super.registry);
}
class Linter1 extends LintRule {
Linter1({
required super.name,
required super.group,
required super.description,
required super.details,
});
}
class Linter1Impl extends SimpleAstVisitor<void> {
@override
void visitMethodDeclaration(MethodDeclaration node) {
doSomething(node);
}
}
class Linter2 extends LintRule {
Linter2({
required super.name,
required super.group,
required super.description,
required super.details,
});
}
class Linter2Impl extends SimpleAstVisitor<void> {
@override
void visitExpressionStatement(ExpressionStatement node) {
doSomething2(node);
super.visitExpressionStatement(node);
}
}
void doSomething(MethodDeclaration node) {}
void doSomething2(ExpressionStatement node) {} One visitor per lint benchmark: // Import BenchmarkBase class.
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:benchmark_harness/benchmark_harness.dart';
void main() {
MultipleVisitorBenchmark().report();
}
// Create a new benchmark by extending BenchmarkBase
class MultipleVisitorBenchmark extends BenchmarkBase {
MultipleVisitorBenchmark() : super('Multiple visitors');
late ParseStringResult result;
// The benchmark code.
@override
void run() {
result.unit.accept(Visitor1());
result.unit.accept(Visitor2());
}
// Not measured setup code executed prior to the benchmark runs.
@override
void setup() {
result = parseFile(
path:
'/Users/remirousselet/dev/invertase/custom_lint/packages/custom_lint/lib/custom_lint.dart',
featureSet: FeatureSet.latestLanguageVersion(),
);
}
}
class Visitor1 extends RecursiveAstVisitor<void> {
@override
void visitMethodDeclaration(MethodDeclaration node) {
doSomething(node);
super.visitMethodDeclaration(node);
}
}
class Visitor2 extends RecursiveAstVisitor<void> {
@override
void visitExpressionStatement(ExpressionStatement node) {
doSomething2(node);
super.visitExpressionStatement(node);
}
}
void doSomething(MethodDeclaration node) {}
void doSomething2(ExpressionStatement node) {} From my tests, the shared visitor is on average 10% slower than having one visitor per lint as I thought |
I ran your tests, and I do see a ≈7 microsecond increase with the shared visitor. I believe this is the O(2c) overhead that I mentioned before. The problem with your perf test is you only have two rules, which doesn't allow the O(rn) complexity of the multiple visitors to show itself. I ran your same tests, but with more lint rules: Perf test code// Import BenchmarkBase class.
import 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/src/lint/linter.dart';
import 'package:analyzer/src/lint/linter_visitor.dart';
import 'package:benchmark_harness/benchmark_harness.dart';
const ruleCount = 10; // Change to any number
void main() {
SharedVisitorBenchmark().report();
MultipleVisitorBenchmark().report();
}
// Create a new benchmark by extending BenchmarkBase
class SharedVisitorBenchmark extends BenchmarkBase {
SharedVisitorBenchmark() : super('Shared visitors');
late ParseStringResult result;
late NodeLintRegistry registry;
// The benchmark code.
@override
void run() {
result.unit.visitChildren(SharedVisitor(registry));
}
// Not measured setup code executed prior to the benchmark runs.
@override
void setup() {
registry = NodeLintRegistry(false);
for (var i = 0; i < ruleCount; i ++) {
registry.addAdjacentStrings(
MyLinter(name: 'linter'),
LinterImpl(),
);
}
result = parseFile(
path:
'/Users/H010675/StudioProjects/personal/projects/framework_example/lib/main.dart',
featureSet: FeatureSet.latestLanguageVersion(),
);
}
}
class SharedVisitor extends LinterVisitor {
SharedVisitor(super.registry);
}
class MyLinter extends LintRule {
MyLinter({
required super.name,
}) : super(
group: Group.style,
description: name,
details: 'details',
);
}
class LinterImpl extends SimpleAstVisitor<void> {
@override
void visitAdjacentStrings(AdjacentStrings node) {
doSomething(node);
}
}
// Create a new benchmark by extending BenchmarkBase
class MultipleVisitorBenchmark extends BenchmarkBase {
MultipleVisitorBenchmark() : super('Multiple visitors');
late ParseStringResult result;
// The benchmark code.
@override
void run() {
for (var i = 0; i < ruleCount; i ++) {
result.unit.visitChildren(IndividualVisitor());
}
}
// Not measured setup code executed prior to the benchmark runs.
@override
void setup() {
result = parseFile(
path:
'/Users/H010675/StudioProjects/personal/projects/framework_example/lib/main.dart',
featureSet: FeatureSet.latestLanguageVersion(),
);
}
}
class IndividualVisitor extends RecursiveAstVisitor<void> {
@override
void visitAdjacentStrings(AdjacentStrings node) {
doSomething(node);
super.visitAdjacentStrings(node);
}
}
void doSomething(AstNode node) {}
With 2 rules With 10 rules With 100 rules As you can see, adding more lint rules significantly increases run time with individual visitors due to the O(rn) complexity. This is why the Dart SDK shares a visitor. It makes it so adding more rules doesn't increasingly pollute the time it takes to compute lints. I'd say a 7 microsecond increase when you have a few lints is worth it running 25 times faster with 100 lints? |
Before I forget, here's the links for what I was talking about when I said this. Here's all the "context agnostic" code corrections: And here's where the lints are mapped to the corrections they can use: It's a cool pattern worth considering, but could be complicated for package users because then they have to design their corrections to work in any context instead of just for the lint. It would be really awesome, though, if we could reuse these Dart corrections instead of making our own, but they are in |
I see, thanks for the counter benchmark. Stil I don't like the added complexity, and with such an approach, there's no way for custom_lint to await the execution of the lint logic. |
If you're referring to knowing when all lints are done, then that is after If you're referring to knowing when a single lint is done, then that indeed seems impossible since all lints are mixed together into the same visitor. But why would you need to know this? |
No, because some lints might want to be async. |
Not a single dart lint is asynchronous. Why would our custom lints need to be? They can do everything synchronously. |
I've made several asynchronous lints before. It's not impossible that I missed some util that would've enabled me to make it synchronously instead of async. But I know the average user will try to make an async lint at some point. And they will probably take a long time to realize that their async logic has broken the custom_lint CLI Being able to handle asynchronous lints, even if optional, would add some safeguard. |
A couple options come to mind.
If no methods of the LintRule may be a |
Yeah I'm experimenting with those right now. I'll see what I can come-up with. |
Do we really need to register an AstVisitor for the shared Visitor btw? I was thinking that we could do: register(registry) {
registry.onVisitVarialeDeclaration((variableDeclaration) {
reporter.reportError(...);
});
} This removes the need for making a separate AstVisitor (which is apparently always a SimpleAstVisitor in the SDK), removing the need for registering each node listener. In terms of organization, it's a bit worse. But it's probably more intuitive for the average Dart dev. Otherwise, I'm 100% sure many will forget the "register" step. Even I am sure I'll make the mistake at some point. |
Yeah, I agree the registering needs to be more intuitive. It's extremely easy to forget to keep it up to date, and then when you miss something, there's no indication that's the problem. Only way I could think of doing it more automatically is something like this that detects if a visit method has been overridden: Classesclass LintRuleBase extends LintRule {
LintRuleBase({
required super.name,
required super.group,
required super.description,
required super.details,
});
}
abstract class ExampleLintRule extends SimpleAstVisitor<void> {
final String message;
final String code;
final String? url;
ExampleLintRule({
required this.message,
required this.code,
this.url,
});
late LintRule rule = LintRuleBase(
description: '',
details: message,
group: Group.style,
name: code,
);
void registerNodeVisitors(NodeLintRegistry registry) {
if (visitClassDeclarations != null) {
registry.addClassDeclaration(rule, this);
}
// About 150 more if statements :(
}
Function(ClassDeclaration node)? get visitClassDeclarations => null;
// 150 more getters :'(
// Had to add an "s" to not conflict with the name of the actual method below.
@override
void visitClassDeclaration(ClassDeclaration node) {
visitClassDeclarations?.call(node);
}
// 150 more visit methods D:
} Then a developer uses it like this: Usageclass FinalProviders extends ExampleLintRule {
FinalProviders() : super(code: 'some_lint_code', message: 'Fix this');
@override
Function(ClassDeclaration node) get visitClassDeclarations => (ClassDeclaration node) {
print('Hello world');
};
} But the syntax of overriding a getter that returns a method is strange, not to mention the lint rule class would be massive. Your example may just be the way to go, for lack of a better option. |
Although, wait, the NodeLintRegistry's addSomethingDeclaration methods expect to be given a LintRule and a Visitor. How will you get those? |
I wouldn't use the SDK's NodeLintRegistry. I'd make a custom implementation with a different prototype. |
That could work then |
For reference, I ended up with the following. That's going to be the new example (with a lint + fix + assist) I've used the shared visitor logic for assists/fixes too. import 'package:analyzer/error/error.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
const _providerBaseChecker =
TypeChecker.fromName('ProviderBase', packageName: 'riverpod');
PluginBase createPlugin() => _RiverpodLint();
class _RiverpodLint extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) => [
PreferFinalProviders(),
];
@override
List<Assist> getAssists() => [_ConvertToStreamProvider()];
}
class PreferFinalProviders extends DartLintRule {
PreferFinalProviders() : super(code: _code);
static const _code = LintCode(
name: 'riverpod_final_provider',
problemMessage: 'Providers should be declared using the `final` keyword.',
);
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
LintContext context,
) {
context.registry.addVariableDeclaration((node) {
final element = node.declaredElement;
if (element == null ||
element.isFinal ||
!_providerBaseChecker.isAssignableFromType(element.type)) {
return;
}
reporter.reportErrorForElement(PreferFinalProviders._code, element);
});
}
@override
List<Fix> getFixes() => [_MakeProviderFinalFix()];
}
class _MakeProviderFinalFix extends DartFix {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
LintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
final errorRange = SourceRange(analysisError.offset, analysisError.length);
context.registry.addVariableDeclarationList((node) {
final nodeRange = SourceRange(node.offset, node.length);
if (!errorRange.intersects(nodeRange)) return;
final changeBuilder = reporter.createChangeBuilder(
priority: 1,
message: 'Make provider final',
);
changeBuilder.addDartFileEdit((builder) {
final nodeKeyword = node.keyword;
final nodeType = node.type;
if (nodeKeyword != null) {
// var x = ... => final x = ...
builder.addSimpleReplacement(
SourceRange(nodeKeyword.offset, nodeKeyword.length),
'final',
);
} else if (nodeType != null) {
// Type x = ... => final Type x = ...
builder.addSimpleInsertion(nodeType.offset, 'final ');
}
});
});
}
}
class _ConvertToStreamProvider extends DartAssist {
@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
LintContext context,
SourceRange target,
) {
context.registry.addVariableDeclaration((node) {
// Check that the visited node is under the cursor
if (!target.intersects(SourceRange(node.offset, node.length))) {
return;
}
// verify that the visited node is a provider, to only show the assist on providers
final element = node.declaredElement;
if (element == null ||
element.isFinal ||
!_providerBaseChecker.isAssignableFromType(element.type)) {
return;
}
final changeBuilder = reporter.createChangeBuilder(
priority: 1,
message: 'Convert to StreamProvider',
);
// TODO implement change
});
}
} |
Now we can do: # analysis_options.yaml
custom_lint:
rules:
- riverpod_final_provider: false |
Awesome, this is really great!
Could the reporter be given the
Can/should this be done from within the package?
I'm assuming all rules will be enabled by default unless disabled, right? Hopefully this would be clear to users that it's the opposite from official dart lints. |
The ErrorReporter class is from the SDK. I didn't write this one. Using the static variable is not necessary. Technically there's a In this variant, using
I thought about it, but I'm skeptical. I figured someone may want to visit things that are outside of the cursor too.
All lints are enabled by default, and there's a flag for changing the default. So you can do: custom_lint:
enable_all_lint_rules: false
rules:
- my_rule |
Continuing on this, one thought I had was to keep doing the So we could do: // Check that the visited node is under the cursor
if (!target.intersects(node.sourceRange)) {
return;
} The gain is minor though. |
I'm also probably going to implement a And I'm considering implementing auto-completion in the analysis_options.yaml, to auto-complete lint names. |
I think you could use this: It would then be |
Fair enough, didn't know it was a thing. |
I'll still include an extension to do "node.sourceRange" |
Yep sounds good |
It's all working on #63 I was playing around with analyzing the non-dart file (which now works fine). I thought I'd find a similar visitor pattern in the SDK for Yaml/Pubspec/AnalysisOptions visitors, but apparently not. I guess I'll add a "PubspecLintRule" & co at a later date. |
Thanks a lot for doing this! |
Thanks for your involvement in this issue too :) |
* Bump version to `0.1.0` * Pull changes from `solid-metrics` repository * Update .gitignore Co-authored-by: Yuri Prykhodko <40931732+solid-yuriiprykhodko@users.noreply.github.com> * Update example/lib/solid_lints_example.dart Co-authored-by: Yuri Prykhodko <40931732+solid-yuriiprykhodko@users.noreply.github.com> * Update `CHANGELOG.md` * Add code checks with GitHub Actions * Add support for the number of parameters metric * Remove DCM references * Change lint message * Simplify configuration and remove redundant code * Fix code comments * Add missing tests * Add `lines_of_code` lint * Fix rule name * Add test and fix an edge case * Update lint to match the specification * Add `avoid_non_null_assertion` lint * Add missing comment * Add `avoid_late_keyword` lint * Update problem message * Add `avoid_global_state` lint * Fix failing tests * Add `avoid_returning_widgets` lint * Use pattern-matching * Simplify `isWidgetReturned` * Update lib/lints/avoid_returning_widgets/avoid_returning_widgets_rule.dart Co-authored-by: Yuri Prykhodko <40931732+solid-yuriiprykhodko@users.noreply.github.com> * Cleanup of `getLintRules` * Fix comment * rename MetricRule -> RuleConfig * add a sceleton for the double-literal-format rule * implement the double-literal-format rule * fix review warnings * implement quick-fix for the double-literal-format rule * fix lint warnings * add avoid-unnecessary-type-assertions rule ('is' part) * fix lint warning * add avoid-unnecessary-type-assertions rule ('whereType' part) * add quick-fix for unnecessary operator is * add quick-fix for unnecessary whereType * fix review warnings; fix a bug * add utility class for type chekcking methods * add constants for is and whereType names * add needed comments * rewrite _isUnnecessaryWhereType with pattern matching * rewrite _areGenericsWithSameTypeArgs with pattern matching * refactoring: move a castTypeInHierarchy to TypeCast class * implement "avoid unnecessary setstate" rule (invertase#44) * implement "avoid unnecessary setstate" rule * fix pr comments * added idea files to gitignore * fix pr comments * Fix merge conflict * Fix tests and improve GitHub workflow * Fix workflow file * Fix conflicts after merge * Remove non-existing import * Add missing rule to tests and remove unnecessary GitHub actions step --------- Co-authored-by: Yaroslav Laptiev <yaroslav.laptiev@wmg.com> Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Add avoid_unnecessary_type_casts_rule * Refactor types_utils, extract common methods * Add fix for avoid_unnecessary_type_casts_rule * Add AvoidUnnecessaryTypeCastsRule.createRule to solid_lints.dart * Fix 'quickFix' for avoid-unnecessary-type-casts * Add tests for avoid-unnecessary-type-casts * Fix formatting * Add more test cases * Group code for more readability * Switch to getters, refactor areGenericsWithSameTypeArgs * Avoid unrelated type assertions (invertase#48) * Add avoid-unrelated-type-assertions rule * Add tests for avoid-unrelated-type-assertions rule --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Feature/newline before return (invertase#52) * Add newline-before-return rule * Fix typos, remove unnecessary negation * Add tests for newline-before-return --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Fixes Issue invertase#54 multiple rule reporting (invertase#55) * Fix newline-before-return * Fix avoid-unrelated-type-assertions * Fix avoid-unnecessary-type-casts --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Feature/no empty block (invertase#53) * Add no-empty-block rule * Add tests no-empty-block rule * Add cyclomatic tests case, fix formatting --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Add no equal then else rule (invertase#56) * Add no-equal-then-else rule * Add tests for no-equal-then-else rule --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Feature/member ordering (invertase#51) * Add required models for member-ordering rule * Add member-ordering rule * Add MIT License comments * Fix parser type mismatch * Add tests for member-ordering and enable rule * Organize imports, ignore member-ordering in unrelated test * Group *_member_group.dart into one directory * Add more test cases * Add more test cases * Add alphabetize test cases * Add tests for alphabetical-by-type option * Ignore no-empty-block in test * Ignore member-ordering in test --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Add avoid unused parameters rule (invertase#49) * Add avoid-unused-parameters rule * Add tests for avoid-unused-parameters rule * Fix function name, remove null check operator and remove negation * Fix multiple reporting * Add more test cases * Fix formatting * Rename method * Add constructors and factories unused params handling * Fix constructors with named field parameters * Simplify rule run method * Fix tests * Fix tests after merge * Fix tests after merge --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Added no magic number rule (invertase#57) * Add no-magic-number rule * Add tests for no-magic-number rule * Fix import lost in merge * Ignore no-magic-number in test --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> * Added prefer condtional expressions rule and fix (invertase#59) * Add prefer-conditional-expressions rule and fix * Add tests for prefer-conditional-expressions rule * fix nested test plugin path * Fix tests after merge * Update lint_test/prefer_conditional_expressions_ignore_nested_test/prefer_conditional_expressions_ignore_nested_test.dart --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Added prefer first rule (invertase#60) * Add prefer-conditional-expressions rule and fix * Add tests for prefer-conditional-expressions rule * fix nested test plugin path * Fix tests after merge * Add prefer-first rule and fix * Add tests for prefer-first rule --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Add prefer last rule (invertase#61) * Add prefer-conditional-expressions rule and fix * Add tests for prefer-conditional-expressions rule * fix nested test plugin path * Fix tests after merge * Add prefer-first rule and fix * Add tests for prefer-first rule * Add prefer-last rule & fix * Add tests for prefer-last rule --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Add prefer match file name (invertase#62) * Add prefer-conditional-expressions rule and fix * Add tests for prefer-conditional-expressions rule * fix nested test plugin path * Fix tests after merge * Add prefer-first rule and fix * Add tests for prefer-first rule * Add prefer-last rule & fix * Add tests for prefer-last rule * Add prefer-match-file-name rule * Add tests for prefer-match-file-name rule * Typos, naming * Typos, naming * rm todo comment * Update lint_test/prefer_match_file_name_test.dart * Update lint_test/prefer_match_file_name_test.dart --------- Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> Co-authored-by: Yurii Prykhodko <yurii.prykhodko@solid.software> * Rename custom lints to use snake_case (invertase#66) * Rename custom lints to use snake_case * Rename missed parameter --------- Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Take into account the constructor initializer in the avoid_unused_parameters rule (invertase#67) Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Fix linter issues * Bump custom_lint_builder version * Update LICENSE to include third party code license * Allow magic numbers for default values (invertase#72) * Allow magic numbers for default values * More tests --------- Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Ignore magic number in constructor initializer (invertase#73) * Ignore magic number in constructor initializer * Minor naming improvement --------- Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Remove DCM steps from readme * Fix avoid-late if initialized (invertase#71) * Implement proper-super-calls (invertase#77) * Fix avoid-late if initialized * Update lint_test/avoid_late_keyword_test.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Custom avoid-late * Fix naming * Apply suggestions from code review Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Avoid late simplified * Update lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Avoid-late ignored_types * Avoid-late ignored_types formatted * Update lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Avoid-late ignored_types fix * Avoid-late ignored_types Fix * Avoid-late allow_initialized testcases * Update lint_test/avoid_late_keyword_allow_initialized_test/pubspec.yaml Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Update lib/lints/avoid_late_keyword/models/avoid_late_keyword_parameters.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Allow subclasses for avoid-late whitelist * Fix naming * Short-circuit of there's no ignored types * Short-circuit earlier * Update lib/lints/avoid_late_keyword/avoid_late_keyword_rule.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Avoid-late ignored_types tests * Avoid-late add testcases * Proper-super-calls impl * Proper-super-calls format * Apply suggestions from code review Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Proper-super-calls refactoring * Update lib/lints/proper_super_calls/proper_super_calls_rule.dart Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> * Proper-super-keyword annotation check * Proper-super-keyword format * Proper-super-calls cleanup --------- Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> Co-authored-by: Yurii Prykhodko <yurii.prykhodko@solid.software> * Ignore magic numbers in widget parameters (invertase#74) * Ignore magic numbers in widget parameters * Add lint parameter to ignore magic numbers in widget params * Improve the allowed_in_widget_params parameter to exclude magic numbers in nested objects of Widget parameters * Add tests for disabled allowed_in_widget_params lint parameter * Remove unnecessary dependency --------- Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> * Set default severity level to warning (invertase#78) Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> --------- Co-authored-by: Nikodem Bernat <n.bernat@outlook.com> Co-authored-by: Vadym Khokhlov <xvadima@ukr.net> Co-authored-by: Yaroslav <43727448+laptevw@users.noreply.github.com> Co-authored-by: Yaroslav Laptiev <yaroslav.laptiev@wmg.com> Co-authored-by: vladimir-beloded <x-volodymyr.beloded@transcarent.com> Co-authored-by: Denis Bogatirov <denis.bohatyrov@computools.com> Co-authored-by: DenisBogatirov <denis.bogatirov@gmail.com> Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> Co-authored-by: solid-vovabeloded <41615621+solid-vovabeloded@users.noreply.github.com> Co-authored-by: Illia Romanenko <442086+illia-romanenko@users.noreply.github.com> Co-authored-by: maxxlab <42914015+maxxlab@users.noreply.github.com>
Issue:
The current
PluginBase
class requires overriding thegetLints
method and returning aStream<Lint>
without providing guidance or structure on how all the custom lints are to be provided. Users of this package must design their own way to go from aResolvedUnitResult
toyield
ingLints
.To be more clear, when creating a single custom lint, the current implementation works fine (like your final providers lint in the example folder). The issue happens when trying to define multiple lints. How do we make lints easy to add/remove? How do we avoid
getLints
from becoming a giant method full of different lints? How do we decouple lints but also let them share an AstVisitor to speed up processing?Essentially:
PluginBase
tooSolution:
Expect users to supply lint rules instead of a
PluginBase
.I anticipate showing code will be the best way to explain this.
This code would be added to the custom_lint package (it's just an example, doesn't need to be done this way).
Then for a user to define a lint rule, it's just a matter of defining an
ExampleLintRule
.Here's the rewritten version of the final provider rule in the example folder.
The benefits of this are:
resolvedUnitResult.libraryElement.topLevelElements.where(...).where(...)
, we visit variable declarations and use an if statementNodeLintRegistry
so thatPluginBase
can create a single AstVisitor instead of each rule visiting the tree itself (they use aVisitor
, but, it doesn't actually do any visiting...)My code is a proof of concept and doesn't have to be the route you go, of course. You may use any, none, or all of it.
Conclusion:
The interaction with the custom_lint package can be greatly simplified by allowing users to define rules instead of a
PluginBase
.In fact, with this approach, you could modify the
startPlugin
method to require a list of rules instead of aPluginBase
. ThestartPlugin
method would create thePluginBase
itself and give it the rules (if aPluginBase
is even required anymore).Also, this could possibly solve #50, or at least get closer.
Depending on the implementation, this doesn't have to be a breaking change.
The text was updated successfully, but these errors were encountered: