-
Notifications
You must be signed in to change notification settings - Fork 231
First pass at strict dependencies #1508
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
Conversation
/cc @kevmoo |
Crazy excited to see this! |
One use-case that might be affected is to have a package that is only used through |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally we only make new validations warnings, because we don't want to get into a situation where users are avoiding updating pub because older versions will publish their packages and newer versions won't. We may upgrade some of these to errors for 2.0.
Validate you haven't declared dependencies you don't use (?)
I don't like this. It's valid for a package to only use assets from its dependencies, for example.
class StrictDependenciesValidator extends Validator { | ||
StrictDependenciesValidator(Entrypoint entrypoint) : super(entrypoint); | ||
|
||
@override |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pub doesn't use @override
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
import 'package:pub/src/validator.dart'; | ||
|
||
/// Returns a map of files -> package names imported or exported within [files]. | ||
Map<String, Iterable<String>> _findUsedPackages(Iterable<String> files) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this an instance method of StrictDependenciesValidator
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
var packageNames = <String, Iterable<String>>{}; | ||
for (var file in files) { | ||
var usedPackages = <String>[]; | ||
var compilationUnit = parseDirectives(new File(file).readAsStringSync()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pub uses utilities in lib/src/io.dart
to do filesystem stuff with logging—in this case, readTextFile()
.
Also, you can use parseImportsAndExports()
from lib/src/dart.dart
to do this more easily.
This should gracefully handle a parse failure. It's not great UI if pub lish
just crashes with an analysis error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks.
Added a simple try/catch but let me know if you want something more sophisticated...
|
||
@override | ||
Future validate() { | ||
return new Future.sync(() async { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not make the method itself async
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
return new Future.sync(() async { | ||
var declared = new Set<String>() | ||
..addAll(entrypoint.root.dependencies.map((d) => d.name)) | ||
..addAll(entrypoint.root.devDependencies.map((d) => d.name)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should also verify that files in lib and bin don't use dev dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that always true? For example I've seen packages that have something like
lib/src/test_utils.dart
I'm not saying it's right, but this seems like a much bigger change than just enforcing that you've declared your dependencies so far and I'd rather tackle it in a future improvement with some consensus.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely invalid for any Dart file in lib/
to import anything that's not a non-dev dependency. The fact that people may be doing this now is all the more reason to add a warning and notify them that it violates pub's dependency semantics.
} | ||
|
||
main() { | ||
group('should consider a package valid if', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"if it"
Concatenated test names should generally read as sentences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
"not_declared": "^1.2.3" | ||
}, sdk: ">=1.8.0 <2.0.0") | ||
]).create(); | ||
d.file(path.join(appPath, 'lib', 'library.dart'), r''' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nest this in the previous d.dir()
call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
integration('declares an "import" as a dependency', () { | ||
d.dir(appPath, [ | ||
d.libPubspec("test_pkg", "1.0.0", deps: { | ||
"not_declared": "^1.2.3" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Declaring a dependency on a package named "not_declared" is pretty confusing :p.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to @kevmoo's favorite, silly_monkey
for (final directive in compilationUnit.directives) { | ||
if (directive is UriBasedDirective) { | ||
usedPackages | ||
.add(Uri.parse(directive.uri.stringValue).pathSegments.first); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if it's not a package:
import?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks.
expectValidationWarning(strictDeps); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also test:
- Files outside
lib/
. - A matching dev dependency.
- A non-Dart file that looks like a Dart file that contains an invalid dependency.
- A Dart file with a parse error early enough that
parseDirectives()
runs into it. - A Dart file with a relative import.
- A Dart file with an absolute, non-
package:
import. - Dart files with invalid
package:
URIs like"package:"
,"package:foo"
,"package:/"
,"package:/]"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not add a file outside lib/
as per my comment above, nor invalid packages - I could use some guidance on the latter if there is already something in pub that can do simple validation or if I should just check the parsed Uri
.
The rest are done!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not add a file outside
lib/
as per my comment above
Which comment?
I could use some guidance on the latter if there is already something in pub that can do simple validation or if I should just check the parsed
Uri
.
All the validators were written before the analyzer library existed, so we currently validate nothing that involves parsing Dart code. For invalid URLs I think we should probably just emit a warning that says "Invalid URL" and points to the source span.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not add a file outside
lib/
as per my comment aboveWhich comment?
Got outdated, but I am not sure how to do this. Do you have any pointers?
I could use some guidance on the latter if there is already something in pub that can do simple validation or if I should just check the parsed
Uri
.All the validators were written before the analyzer library existed, so we currently validate nothing that involves parsing Dart code. For invalid URLs I think we should probably just emit a warning that says "Invalid URL" and points to the source span.
How can I determine an invalid URL? Other than reinventing the world.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing you already parse URIs with Uri.parse
, or similar. That should ensure that the URI is actually valid URIs.
Package URIs have a more restricted syntax that you might also want to check. To validate those, you can either reuse the code in package:package_config or aim lower (that code is written for .packages
files which are very specific, actual imports might want to be more permissive).
The base rules for package URIs is:
- Starts with
package:
. - Has no authority part.
- The path doesn't start with a
/
, - but with a path segment containing at least one non-
.
character and no:
or%
characters, - followed by a
/
. - Has no query part (but that's probably not important).
You can check that at the string level with a RegExp:
var pchar = r"!$&-\-0-9;=@-Z_a-z~";
const pcharDot = r"!$&-.0-9;=@-Z_a-z~";
var packageUriRE = new RegExp("^package:[$pcharDot]*[$pchar]$[pcharDot]*/");
or on a parsed URI:
const pchar = r"!$&-\-0-9;=@-Z_a-z~";
const pcharDot = r"!$&-.0-9;=@-Z_a-z~";
final packagePathUriRE = new RegExp("^[$pcharDot]*[$pchar]$[pcharDot]*/");
bool isPackageUriValid(Uri uri) {
return (uri.scheme == "package" &&
!uri.hasAuthority &&
uri.path.matches(packageUriPathRE) &&
!uri.hasQuery);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got outdated, but I am not sure how to do this. Do you have any pointers?
You could add a _DependencyUse.inLib
getter, only check against normal dependencies if it's set, then check against dev dependencies when generating a message to provide more information.
How can I determine an invalid URL? Other than reinventing the world.
I don't think you need to do anything as complex as Lasse is suggesting. Uri.parse()
will throw a FormatError
if the URL doesn't parse at all, and if it does I think it's sufficient to check that the package name isn't null
—any other weird names will fail to match any dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, I've implemented that.
return new Future.sync(() async { | ||
var declared = new Set<String>() | ||
..addAll(entrypoint.root.dependencies.map((d) => d.name)) | ||
..addAll(entrypoint.root.devDependencies.map((d) => d.name)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's definitely invalid for any Dart file in lib/
to import anything that's not a non-dev dependency. The fact that people may be doing this now is all the more reason to add a warning and notify them that it violates pub's dependency semantics.
expectValidationWarning(strictDeps); | ||
}); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not add a file outside
lib/
as per my comment above
Which comment?
I could use some guidance on the latter if there is already something in pub that can do simple validation or if I should just check the parsed
Uri
.
All the validators were written before the analyzer library existed, so we currently validate nothing that involves parsing Dart code. For invalid URLs I think we should probably just emit a warning that says "Invalid URL" and points to the source span.
|
||
import 'package:analyzer/analyzer.dart'; | ||
import 'package:pub/src/solver/version_solver.dart'; | ||
import 'package:path/path.dart' as path; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as p
. Pub is kind of inconsistent about this, but that's only because it's old—new files always use as p
.
usedPackages | ||
.add(Uri.parse(directive.uri.stringValue).pathSegments.first); | ||
class StrictDependenciesValidator extends Validator { | ||
static bool _isDartFile(String file) => path.extension(file) == '.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just inline this.
if (directive is UriBasedDirective) { | ||
usedPackages | ||
.add(Uri.parse(directive.uri.stringValue).pathSegments.first); | ||
class StrictDependenciesValidator extends Validator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document this, and also the class below and its members.
: _parsedUri = Uri.parse(directive.uri.stringValue), | ||
_directive = directive; | ||
|
||
bool get isPubPackage => _parsedUri.scheme == 'package'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is kind of misleading, since everything (including the entrypoint) is a pub package. Maybe isPackageUrl
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
String get package => _parsedUri.pathSegments.first; | ||
|
||
String toErrorMessage() { | ||
return new SourceFile(_contents, url: _file) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing that you're creating the SourceFile
here rather than sharing one between all the directives for a given file so that you avoid parsing the line endings in the common case of a file that has only valid directives? If so, mention that in a comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
.span(_directive.offset, _directive.length) | ||
.message( | ||
'$_file imports $package, but this package doesn\'t depend ' | ||
'on $package'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: "."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -17,30 +17,88 @@ Validator strictDeps(Entrypoint entrypoint) { | |||
} | |||
|
|||
main() { | |||
group('should consider a package valid if', () { | |||
group('should consider a package valid if it', () { | |||
setUp(d.validPackage.create); | |||
|
|||
integration('looks normal', () => expectNoValidationError(strictDeps)); | |||
|
|||
integration('declares an "import" as a dependency', () { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't need to duplicate every test, but we should probably have at least one test for an export
that doesn't produce a warning.
|
||
String toErrorMessage() { | ||
return new SourceFile(_contents, url: _file) | ||
.span(_directive.offset, _directive.length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.span()
takes a start and an end, not a start and a length. It's also 0-based, so make sure that the analyzer is as well or add some - 1
s if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@nex3 I think I've tackled everything you asked for - PTAL. If there are only incremental things left I'd be happy to fix them in more follow-up PRs since it's not yet enabled by default or behind a flag or anything. Let me know. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there are only incremental things left I'd be happy to fix them in more follow-up PRs since it's not yet enabled by default or behind a flag or anything. Let me know.
I generally prefer to keep master
in a releasable state if at all possible, especially for pub
where master is frequently pulled into the SDK and released.
|
||
/// Validates that Dart source files only import declared dependencies. | ||
class StrictDependenciesValidator extends Validator { | ||
static Iterable<String> _combine(Iterable<String> a, Iterable<String> b) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd name this combineIterables()
and put it in utils.dart
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
import 'package:pub/src/dart.dart'; | ||
import 'package:pub/src/entrypoint.dart'; | ||
import 'package:pub/src/io.dart'; | ||
import 'package:pub/src/log.dart'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Import this with the log
prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
directives = parseImportsAndExports(contents, name: file); | ||
} on AnalyzerErrorGroup catch (e, s) { | ||
// Ignore files that do not parse. | ||
exception(e, s); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.exception()
will print the exception message at the "error" level, which is visible by default. I think we want to hide these by default—it'll be pretty confusing if pub lish
surfaces a bunch of parse errors that aren't in the typical warning or error format. I'd just do:
log.fine(getErrorMessage(error));
log.fine(new Chain.forTrace(stackTrace).terse);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
StrictDependenciesValidator(Entrypoint entrypoint) : super(entrypoint); | ||
|
||
Set<String> _dependencies; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please document private members.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
.toSet(); | ||
} | ||
return entrypoint.root.name == package || _dependencies.contains(package); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than making these instance methods, consider creating the two sets in validate()
and just passing them to the two _validate...()
functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
bool get isUriValid => _uri != null && _uri.pathSegments.length >= 2; | ||
|
||
/// Returns whether the directive references a pub package. | ||
bool get isPackageUrl => _uri.scheme == 'package'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if _uri
is null?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simplified, so this is gone now.
String uriInvalidMessage() { | ||
assert(!isUriValid); | ||
var uri = _directive.uri.stringValue; | ||
return _toMessage('$_file references an invalid URI: $uri'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd just say Invalid URL.
here. The source span message will already show the URL and the filename.
Fun fact: the term "URI" is deprecated. Dart is very inconsistent about this, but I'm trying to use "URL" where possible. Maybe we can change the class name in 2.0...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
/// Returns an error message saying the package is not listed in dependencies. | ||
String dependencyMissingMessage() { | ||
return _toMessage( | ||
'$_file imports $package, but this package doesn\'t depend on $package.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package doesn't depend on $package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
/// Returns an error message saying the package should be in `dependencies`. | ||
String dependencyMisplaceMessage() { | ||
return _toMessage( | ||
'$_file imports $package, but is only listed in `devDependencies`. Files ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$package is a dev dependency. Packages used in ${p.split(p.relative(_file)).first}/ must be declared as
normal dependencies.
The newline is important—we try to wrap publish messages around 80 characters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
]).create(); | ||
expectValidationWarning(strictDeps); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still needs no-warning tests for:
- An
export
of a dependency. - A top level
.dart
file that contains an invalid dependency. - A non-Dart file that looks like a Dart file that contains an invalid dependency.
And a warning test for Dart files with URIs like "package:"
, "package:foo"
, and "package:/]"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Nice catch on the non-Dart file.
uri.scheme == 'package' && | ||
uri.pathSegments.length < 2 || | ||
uri.pathSegments.any((s) => s.isEmpty)) { | ||
warnings.add('Invalid URL'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you moved this out here, you lost the source span context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pulled it out to a static function to re-use here with minimal boiler.
if (uri == null || | ||
uri.scheme == 'package' && | ||
uri.pathSegments.length < 2 || | ||
uri.pathSegments.any((s) => s.isEmpty)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please parenthesize sub-expressions with different boolean operators. I'm sure the order of operation works out here, but it's a lot harder to visually verify that without explicit grouping.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I understand. Done.
return _toMessage( | ||
'$_file imports $package, but this package doesn\'t depend on $package.' | ||
); | ||
return _toMessage('This packagee doesn\'t depend on $package.'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"package"
]).create(); | ||
expectValidationWarning(strictDeps); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We still need to test for package:/]
. This is meaningfully different because it's invalid URL syntax, whereas the rest of these are invalid only as package:
URLs in particular.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Thanks for working so hard on this, Matan! |
First step of #343.
Implemented
StrictDependenciesValidator
, which crawls all source files, finds all referencedimport
andexport
directives, and compares them against all declareddependencies
anddev_dependencies
.IMO this should be an error, not a warning, but wanted input first to see if I've possibly missed legitimate use cases where people should be able to override and upload anyway. Should this also be added as a flag to
pub
to opt-in or just added by default? I didn't do anything but implement the validator yet.Future work:- [ ] Validate you haven't declared dependencies you don't use (?)