| // Copyright (c) 2017, 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/error/error.dart'; | |
| import 'package:analyzer/error/listener.dart'; | |
| import 'package:analyzer/file_system/file_system.dart'; | |
| import 'package:analyzer/src/generated/source.dart'; | |
| import 'package:analyzer/src/pubspec/pubspec_warning_code.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:source_span/src/span.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| class PubspecValidator { | |
| /** | |
| * The name of the sub-field (under `flutter`) whose value is a list of assets | |
| * available to Flutter apps at runtime. | |
| */ | |
| static const String ASSETS_FIELD = 'assets'; | |
| /** | |
| * The name of the field whose value is a map of dependencies. | |
| */ | |
| static const String DEPENDENCIES_FIELD = 'dependencies'; | |
| /** | |
| * The name of the field whose value is a map of development dependencies. | |
| */ | |
| static const String DEV_DEPENDENCIES_FIELD = 'dev_dependencies'; | |
| /** | |
| * The name of the field whose value is a specification of Flutter-specific | |
| * configuration data. | |
| */ | |
| static const String FLUTTER_FIELD = 'flutter'; | |
| /** | |
| * The name of the field whose value is the name of the package. | |
| */ | |
| static const String NAME_FIELD = 'name'; | |
| /** | |
| * The resource provider used to access the file system. | |
| */ | |
| final ResourceProvider provider; | |
| /** | |
| * The source representing the file being validated. | |
| */ | |
| final Source source; | |
| /** | |
| * Initialize a newly create validator to validate the content of the given | |
| * [source]. | |
| */ | |
| PubspecValidator(this.provider, this.source); | |
| /** | |
| * Validate the given [contents]. | |
| */ | |
| List<AnalysisError> validate(Map<dynamic, YamlNode> contents) { | |
| RecordingErrorListener recorder = new RecordingErrorListener(); | |
| ErrorReporter reporter = new ErrorReporter(recorder, source); | |
| _validateDependencies(reporter, contents); | |
| _validateFlutter(reporter, contents); | |
| _validateName(reporter, contents); | |
| return recorder.errors; | |
| } | |
| /** | |
| * Return `true` if an asset (file) exists at the given absolute, normalized | |
| * [assetPath] or in a subdirectory of the parent of the file. | |
| */ | |
| bool _assetExistsAtPath(String assetPath) { | |
| // Check for asset directories. | |
| Folder assetDirectory = provider.getFolder(assetPath); | |
| if (assetDirectory.exists) { | |
| return true; | |
| } | |
| // Else, check for an asset file. | |
| File assetFile = provider.getFile(assetPath); | |
| if (assetFile.exists) { | |
| return true; | |
| } | |
| String fileName = assetFile.shortName; | |
| Folder assetFolder = assetFile.parent; | |
| if (!assetFolder.exists) { | |
| return false; | |
| } | |
| for (Resource child in assetFolder.getChildren()) { | |
| if (child is Folder) { | |
| File innerFile = child.getChildAssumingFile(fileName); | |
| if (innerFile.exists) { | |
| return true; | |
| } | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Return a map whose keys are the names of declared dependencies and whose | |
| * values are the specifications of those dependencies. The map is extracted | |
| * from the given [contents] using the given [key]. | |
| */ | |
| Map<dynamic, YamlNode> _getDeclaredDependencies( | |
| ErrorReporter reporter, Map<dynamic, YamlNode> contents, String key) { | |
| YamlNode field = contents[key]; | |
| if (field == null || (field is YamlScalar && field.value == null)) { | |
| return <String, YamlNode>{}; | |
| } else if (field is YamlMap) { | |
| return field.nodes; | |
| } | |
| _reportErrorForNode( | |
| reporter, field, PubspecWarningCode.DEPENDENCIES_FIELD_NOT_MAP, [key]); | |
| return <String, YamlNode>{}; | |
| } | |
| /** | |
| * Report an error for the given node. | |
| */ | |
| void _reportErrorForNode( | |
| ErrorReporter reporter, YamlNode node, ErrorCode errorCode, | |
| [List<Object> arguments]) { | |
| SourceSpan span = node.span; | |
| reporter.reportErrorForOffset( | |
| errorCode, span.start.offset, span.length, arguments); | |
| } | |
| /** | |
| * Validate the value of the required `name` field. | |
| */ | |
| void _validateDependencies( | |
| ErrorReporter reporter, Map<dynamic, YamlNode> contents) { | |
| Map<dynamic, YamlNode> declaredDependencies = | |
| _getDeclaredDependencies(reporter, contents, DEPENDENCIES_FIELD); | |
| Map<dynamic, YamlNode> declaredDevDependencies = | |
| _getDeclaredDependencies(reporter, contents, DEV_DEPENDENCIES_FIELD); | |
| for (YamlNode packageName in declaredDevDependencies.keys) { | |
| if (declaredDependencies.containsKey(packageName)) { | |
| _reportErrorForNode(reporter, packageName, | |
| PubspecWarningCode.UNNECESSARY_DEV_DEPENDENCY, [packageName.value]); | |
| } | |
| } | |
| } | |
| /** | |
| * Validate the value of the optional `flutter` field. | |
| */ | |
| void _validateFlutter( | |
| ErrorReporter reporter, Map<dynamic, YamlNode> contents) { | |
| YamlNode flutterField = contents[FLUTTER_FIELD]; | |
| if (flutterField is YamlMap) { | |
| YamlNode assetsField = flutterField.nodes[ASSETS_FIELD]; | |
| if (assetsField is YamlList) { | |
| path.Context context = provider.pathContext; | |
| String packageRoot = context.dirname(source.fullName); | |
| for (YamlNode entryValue in assetsField.nodes) { | |
| if (entryValue is YamlScalar) { | |
| Object entry = entryValue.value; | |
| if (entry is String) { | |
| if (entry.startsWith('packages/')) { | |
| // TODO(brianwilkerson) Add validation of package references. | |
| } else { | |
| bool isDirectoryEntry = entry.endsWith("/"); | |
| String normalizedEntry = | |
| context.joinAll(path.posix.split(entry)); | |
| String assetPath = context.join(packageRoot, normalizedEntry); | |
| if (!_assetExistsAtPath(assetPath)) { | |
| ErrorCode errorCode = isDirectoryEntry | |
| ? PubspecWarningCode.ASSET_DIRECTORY_DOES_NOT_EXIST | |
| : PubspecWarningCode.ASSET_DOES_NOT_EXIST; | |
| _reportErrorForNode( | |
| reporter, entryValue, errorCode, [entryValue.value]); | |
| } | |
| } | |
| } else { | |
| _reportErrorForNode( | |
| reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING); | |
| } | |
| } else { | |
| _reportErrorForNode( | |
| reporter, entryValue, PubspecWarningCode.ASSET_NOT_STRING); | |
| } | |
| } | |
| } else if (assetsField != null) { | |
| _reportErrorForNode( | |
| reporter, assetsField, PubspecWarningCode.ASSET_FIELD_NOT_LIST); | |
| } | |
| if (flutterField.length > 1) { | |
| // TODO(brianwilkerson) Should we report an error if `flutter` contains | |
| // keys other than `assets`? | |
| } | |
| } else if (flutterField != null) { | |
| if (flutterField.value == null) { | |
| // allow an empty `flutter:` section; explicitly fail on a non-empty, | |
| // non-map one | |
| } else { | |
| _reportErrorForNode( | |
| reporter, flutterField, PubspecWarningCode.FLUTTER_FIELD_NOT_MAP); | |
| } | |
| } | |
| } | |
| /** | |
| * Validate the value of the required `name` field. | |
| */ | |
| void _validateName(ErrorReporter reporter, Map<dynamic, YamlNode> contents) { | |
| YamlNode nameField = contents[NAME_FIELD]; | |
| if (nameField == null) { | |
| reporter.reportErrorForOffset(PubspecWarningCode.MISSING_NAME, 0, 0); | |
| } else if (nameField is! YamlScalar || nameField.value is! String) { | |
| _reportErrorForNode( | |
| reporter, nameField, PubspecWarningCode.NAME_NOT_STRING); | |
| } | |
| } | |
| } |