Skip to content
This repository has been archived by the owner on Nov 20, 2024. It is now read-only.

Add a lint for missing files in conditional imports #3080

Merged
merged 5 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 1.16.0

- new lint: `missing_conditional_imports`

# 1.15.0

- new lint: `use_decorated_box`
Expand Down
1 change: 1 addition & 0 deletions example/all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ linter:
- lines_longer_than_80_chars
- list_remove_unrelated_type
- literal_only_boolean_expressions
- missing_conditional_import
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_default_cases
Expand Down
2 changes: 2 additions & 0 deletions lib/src/rules.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import 'rules/library_private_types_in_public_api.dart';
import 'rules/lines_longer_than_80_chars.dart';
import 'rules/list_remove_unrelated_type.dart';
import 'rules/literal_only_boolean_expressions.dart';
import 'rules/missing_conditional_import.dart';
import 'rules/missing_whitespace_between_adjacent_strings.dart';
import 'rules/no_adjacent_strings_in_list.dart';
import 'rules/no_default_cases.dart';
Expand Down Expand Up @@ -291,6 +292,7 @@ void registerLintRules({bool inTestMode = false}) {
..register(LinesLongerThan80Chars())
..register(ListRemoveUnrelatedType())
..register(LiteralOnlyBooleanExpressions())
..register(MissingConditionalImport())
..register(MissingWhitespaceBetweenAdjacentStrings())
..register(NoAdjacentStringsInList())
..register(NoDefaultCases())
Expand Down
70 changes: 70 additions & 0 deletions lib/src/rules/missing_conditional_import.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';

import '../analyzer.dart';

const _desc = r'Missing conditional import.';

const _details = r'''

**DON'T** reference files that do not exist in conditional imports.

Code may fail at runtime if the condition evaluates such that the missing file
needs to be imported.

**BAD:**
```dart
import 'file_that_does_not_exist.dart';
pq marked this conversation as resolved.
Show resolved Hide resolved
```

**GOOD:**
```dart
import 'file_that_exists.dart';
```

''';

class MissingConditionalImport extends LintRule {
MissingConditionalImport()
: super(
name: 'missing_conditional_import',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for naming, I might suggest that we use a name similar to the error code (URI_DOES_NOT_EXIST) such as conditional_uri_does_not_exist. There's a companion error code in the analyzer (URI_HAS_NOT_BEEN_GENERATED) and we might want to have two separate messages for this lint to distinguish between those cases, but that's less clear to me.

(The only convention that I'm aware of for naming lints is that we're now actively discouraging using 'avoid', 'prefer' and other such designations from the style guide because those tend to change over time leaving us with misleading lint names. Other than that, lints should be named to either express the condition they're trying to ensure or the condition they're trying to prevent.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've changed it to conditional_uri_does_not_exist.

There's a companion error code in the analyzer (URI_HAS_NOT_BEEN_GENERATED) and we might want to have two separate messages for this lint to distinguish between those cases, but that's less clear to me.

I don't know much about that so have no opinion, but if you think it would also be useful here I'm happy to add it - it doesn't seem like a complicated change (just changing the code/error depending on whether the uri ends with these suffixes?).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about it a bit more, I'd propose that we leave it as is. If we get feedback from users that a different message would be helpful we can always update it later.

description: _desc,
details: _details,
group: Group.style);

@override
void registerNodeProcessors(
NodeLintRegistry registry, LinterContext context) {
var visitor = _Visitor(this);
registry.addCompilationUnit(this, visitor);
}
}

class _Visitor extends RecursiveAstVisitor<void> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For performance reasons, the linter uses visitors as a dispatch mechanism rather than as a tree walking mechanism. Walking the tree is expensive, so we do it once and effectively run all of the lint rules simultaneously. As a result,

  • _Visitor should extend SimpleAstVisitor,
  • you should use addConfiguration rather than addCompilationUnit on line 43, and
  • you should remove the invocation of super

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, apparently the file I duplicated as a starting point was the only one using a RecursiveAstVisitor (missing_whitespace_between_adjacent_strings.dart) 😄

This makes sense and I've made those changes, thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That rule should definitely be updated. (But obviously not an issue for this PR.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, wasn't sure if there was a reason. I'll file a PR, seems like a trivial change. Thanks!

static const LintCode code = LintCode('missing_conditional_import',
"The target of the conditional URI '{0}' doesn't exist.",
correctionMessage: 'Try creating the file referenced by the URI, or '
'Try using a URI for a file that does exist.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Try" --> "try"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, thanks!


final LintRule rule;

_Visitor(this.rule);

@override
void visitConfiguration(Configuration configuration) {
String? uriContent = configuration.uri.stringValue;
if (uriContent != null) {
Source? source = configuration.uriSource;
if (!(source?.exists() ?? false)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you noted, this will probably work most of the time, but it would be better if we could ask the resource provider. Unfortunately there doesn't appear to be any way to access it currently.

My personal opinion is that we need to update LinterContext to provide access to the resource provider and then pass that in when constructing the visitor. Unfortunately, that's a multi-step process, and I'm not sure we want to delay merging this rule while that happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to go with your decision here. If you think it's worth doing (and I know enough to do it), I'm happy to. Although I'm not sure if having to support package: URIs complicates using the resource provider. This exists() call seemed to handle that (and dart: URIs) for me which I thought might have complicated things :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with leaving it as-is. You might consider adding a comment for future reference, because the fact that it isn't checking for overlays might not occur to a future reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Would this lint be invalidated/updated correctly if the user saved a new file with the name that was used here? Would the server know if a file has import 'file_a.dart' but that file does not exist, that creating a file at that location may require this file to be re-analyzed/linted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know whether it is the case, but it ought to be re-analyzed when a new file is added, and we run lints as part of analyzing a file.

rule.reportLint(configuration.uri,
arguments: [uriContent], errorCode: code);
}
}

super.visitConfiguration(configuration);
}
}
2 changes: 1 addition & 1 deletion test/rule_test_support.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class ExpectedDiagnostic {
if (!diagnosticMatcher(error)) return false;
if (error.offset != offset) return false;
if (error.length != length) return false;
if (messageContains != null && error.message.contains(messageContains!)) {
if (messageContains != null && !error.message.contains(messageContains!)) {
return false;
}

Expand Down
2 changes: 2 additions & 0 deletions test/rules/all.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'avoid_shadowing_type_parameters.dart'
import 'file_names.dart' as file_names;
import 'literal_only_boolean_expressions.dart'
as literal_only_boolean_expressions;
import 'missing_conditional_import.dart' as missing_conditional_import;
import 'missing_whitespace_between_adjacent_strings.dart'
as missing_whitespace_between_adjacent_strings;
import 'non_constant_identifier_names.dart' as non_constant_identifier_names;
Expand Down Expand Up @@ -41,6 +42,7 @@ void main() {
avoid_shadowing_type_parameters.main();
file_names.main();
literal_only_boolean_expressions.main();
missing_conditional_import.main();
missing_whitespace_between_adjacent_strings.main();
non_constant_identifier_names.main();
null_closures.main();
Expand Down
78 changes: 78 additions & 0 deletions test/rules/missing_conditional_import.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../rule_test_support.dart';

main() {
defineReflectiveSuite(() {
defineReflectiveTests(MissingConditionalImportTest);
});
}

@reflectiveTest
class MissingConditionalImportTest extends LintRuleTest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is the preferred style for tests, thanks!

@override
bool get addMetaPackageDep => true;

@override
String get lintRule => 'missing_conditional_import';

test_missingDartLibraries() async {
await assertDiagnostics(
r'''
import ''
if (dart.library.io) 'dart:missing_1'
if (dart.library.html) 'dart:async'
if (dart.library.async) 'dart:missing_2';
''',
[
error(HintCode.UNUSED_IMPORT, 7, 2),
lint('missing_conditional_import', 35, 16,
messageContains: 'dart:missing_1'),
lint('missing_conditional_import', 120, 16,
messageContains: 'dart:missing_2'),
],
);
}

test_missingFiles() async {
newFile('$testPackageRootPath/lib/exists.dart');

await assertDiagnostics(
r'''
import ''
if (dart.library.io) 'missing_1.dart'
if (dart.library.html) 'exists.dart'
if (dart.library.async) 'missing_2.dart';
''',
[
error(HintCode.UNUSED_IMPORT, 7, 2),
lint('missing_conditional_import', 35, 16,
messageContains: 'missing_1.dart'),
lint('missing_conditional_import', 121, 16,
messageContains: 'missing_2.dart'),
],
);
}

test_missingPackages() async {
await assertDiagnostics(
r'''
import ''
if (dart.library.io) 'package:meta/missing_1.dart'
if (dart.library.html) 'package:meta/meta.dart'
if (dart.library.io) 'package:foo/missing_2.dart';
''',
[
error(HintCode.UNUSED_IMPORT, 7, 2),
lint('missing_conditional_import', 35, 29,
messageContains: 'missing_1.dart'),
lint('missing_conditional_import', 142, 28,
messageContains: 'missing_2.dart'),
],
);
}
}
1 change: 1 addition & 0 deletions tool/since/linter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ library_prefixes: 0.1.1
lines_longer_than_80_chars: 0.1.56
list_remove_unrelated_type: 0.1.22
literal_only_boolean_expressions: 0.1.25
missing_conditional_import: 1.16.0
missing_whitespace_between_adjacent_strings : 0.1.110
no_adjacent_strings_in_list: 0.1.30
no_default_cases : 0.1.116
Expand Down