| // Copyright (c) 2024, 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/src/lint/io.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'util/path_utils.dart'; |
| |
| const _categoryNames = { |
| 'binarySize', |
| 'brevity', |
| 'documentationCommentMaintenance', |
| 'effectiveDart', |
| 'errorProne', |
| 'flutter', |
| 'languageFeatureUsage', |
| 'memoryLeaks', |
| 'nonPerformant', |
| 'pub', |
| 'publicInterface', |
| 'style', |
| 'unintentional', |
| 'unusedCode', |
| 'web', |
| }; |
| |
| const String _messagesFileName = 'pkg/linter/messages.yaml'; |
| |
| final Map<String, RuleInfo> messagesRuleInfo = () { |
| var messagesYaml = loadYamlNode(readFile(_messagesYamlPath)); |
| if (messagesYaml is! YamlMap) { |
| throw StateError("The '$_messagesFileName' file is not a YAML map."); |
| } |
| var lintCodes = messagesYaml['LintCode'] as YamlMap?; |
| if (lintCodes == null) { |
| throw StateError( |
| "The '_messagesFileName' file does not have a 'LintCode' section."); |
| } |
| |
| { |
| var lintCodeKeys = lintCodes.keys.cast<String>().toList(growable: false); |
| var lintCodeKeysSorted = lintCodeKeys.sorted(); |
| for (var i = 0; i < lintCodeKeys.length; i++) { |
| if (lintCodeKeys[i] != lintCodeKeysSorted[i]) { |
| throw StateError("The LintCode entries in '_messagesFileName' " |
| "are not sorted alphabetically, starting at '${lintCodeKeys[i]}'."); |
| } |
| } |
| } |
| |
| var builders = <String, _RuleBuilder>{}; |
| for (var MapEntry(key: String uniqueName, value: YamlMap data) |
| in lintCodes.entries) { |
| String sharedName; |
| if (data.containsKey('sharedName')) { |
| sharedName = data['sharedName'] as String; |
| } else { |
| sharedName = uniqueName; |
| } |
| var rule = builders.putIfAbsent(sharedName, () => _RuleBuilder(sharedName)); |
| rule.addEntry(uniqueName, data); |
| } |
| |
| return builders.map((key, value) => MapEntry(key, value.build())); |
| }(); |
| |
| final String _messagesYamlPath = pathRelativeToPackageRoot(['messages.yaml']); |
| |
| class CodeInfo { |
| final String uniqueName; |
| final String problemMessage; |
| final String? correctionMessage; |
| |
| CodeInfo(this.uniqueName, |
| {required this.problemMessage, this.correctionMessage}); |
| } |
| |
| class RuleInfo { |
| final String name; |
| final List<CodeInfo> codes; |
| final String addedIn; |
| final String? removedIn; |
| final Set<String> categories; |
| final bool hasPublishedDocs; |
| final String? documentation; |
| final String deprecatedDetails; |
| |
| RuleInfo( |
| {required this.name, |
| required this.codes, |
| required this.addedIn, |
| required this.removedIn, |
| required this.categories, |
| required this.hasPublishedDocs, |
| required this.documentation, |
| required this.deprecatedDetails}); |
| } |
| |
| // TODO(parlough): Clean up and simplify this validation |
| // once the `messages.yaml` format is more stabilized. |
| class _RuleBuilder { |
| final String sharedName; |
| final List< |
| ({ |
| String uniqueName, |
| String? problemMessage, |
| String? correctionMessage |
| })> _codes = []; |
| String? _addedIn; |
| String? _removedIn; |
| Set<String>? _categories; |
| bool? _hasPublishedDocs; |
| String? _documentation; |
| String? _deprecatedDetails; |
| |
| _RuleBuilder(this.sharedName); |
| |
| bool get _wasRemoved => _removedIn != null; |
| |
| void addEntry(String uniqueName, YamlMap data) { |
| _addCode(uniqueName, data); |
| |
| _setAddedIn(data); |
| _setCategories(data); |
| _setDeprecatedDetails(data); |
| _setDocumentation(data); |
| _setHasPublishedDocs(data); |
| _setRemovedIn(data); |
| } |
| |
| RuleInfo build() => RuleInfo( |
| name: sharedName, |
| codes: _validateCodes(), |
| addedIn: _requireSpecified('addedIn', _addedIn), |
| removedIn: _removedIn, |
| categories: _requireSpecified('categories', _categories, |
| ifNotRemovedFallback: const {}), |
| hasPublishedDocs: _hasPublishedDocs ?? false, |
| documentation: _documentation, |
| deprecatedDetails: |
| _requireSpecified('deprecatedDetails', _deprecatedDetails), |
| ); |
| |
| void _addCode(String name, Map<Object?, Object?> data) { |
| if (_codes.map((code) => code.uniqueName).any((n) => n == name)) { |
| _throwLintError( |
| "Has more than one LintCode with '$name' as its 'uniqueName'."); |
| } |
| |
| String? problemMessage; |
| if (data.containsKey('problemMessage')) { |
| problemMessage = _requireType('problemMessage', data['problemMessage']); |
| } |
| |
| String? correctionMessage; |
| if (data.containsKey('correctionMessage')) { |
| correctionMessage = |
| _requireType('correctionMessage', data['correctionMessage']); |
| } |
| |
| _codes.add(( |
| uniqueName: name, |
| problemMessage: problemMessage, |
| correctionMessage: correctionMessage |
| )); |
| } |
| |
| Never _alreadySpecified(String propertyName) { |
| _throwLintError( |
| "More than one LintCode specified the '$propertyName' property."); |
| } |
| |
| void _requireNotEmpty(String propertyName, String value) { |
| if (value.trim().isEmpty) { |
| _throwLintError("The '$propertyName' value must not be empty."); |
| } |
| } |
| |
| T _requireSpecified<T extends Object>(String propertyName, T? value, |
| {T? ifNotRemovedFallback}) { |
| if (value == null) { |
| if (_wasRemoved && ifNotRemovedFallback != null) { |
| return ifNotRemovedFallback; |
| } |
| _throwLintError("The '$propertyName' property must be specified."); |
| } |
| |
| return value; |
| } |
| |
| T _requireType<T extends Object?>(String propertyName, Object? value) { |
| if (value is! T) { |
| _throwLintError("The '$propertyName' property must be of type '$T'."); |
| } |
| |
| return value; |
| } |
| |
| Iterable<T> _requireTypeForItems<T extends Object?>( |
| String propertyName, Iterable<Object?> items) { |
| for (var item in items) { |
| if (item is! T) { |
| _throwLintError("The items in the '$propertyName' collection must " |
| "each be of type '$T'."); |
| } |
| } |
| |
| return items.cast<T>(); |
| } |
| |
| void _setAddedIn(Map<Object?, Object?> data) { |
| const propertyName = 'addedIn'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| if (_addedIn != null) _alreadySpecified(propertyName); |
| |
| var addedInValue = _requireType<String>(propertyName, value); |
| if (addedInValue.split('.').length != 2) { |
| _throwLintError("The '$propertyName' property must be in " |
| "'major.minor' format, but found '$addedInValue'."); |
| } |
| |
| _addedIn = addedInValue; |
| } |
| |
| void _setCategories(Map<Object?, Object?> data) { |
| const propertyName = 'categories'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| if (_categories != null) _alreadySpecified(propertyName); |
| |
| var categoryValues = _requireType<Iterable<Object?>>(propertyName, value); |
| var categoryStrings = |
| _requireTypeForItems<String>(propertyName, categoryValues); |
| |
| var countWithDuplicates = categoryStrings.length; |
| var categoriesSet = categoryStrings.toSet(); |
| if (countWithDuplicates != categoriesSet.length) { |
| _throwLintError("The '$propertyName' property must not have duplicates."); |
| } |
| |
| for (var category in categoriesSet) { |
| if (!_categoryNames.contains(category)) { |
| _throwLintError("The specified '$category' category is invalid."); |
| } |
| } |
| |
| _categories = categoriesSet; |
| } |
| |
| void _setDeprecatedDetails(Map<Object?, Object?> data) { |
| const propertyName = 'deprecatedDetails'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| if (_deprecatedDetails != null) _alreadySpecified(propertyName); |
| |
| var deprecatedDetails = _requireType<String>(propertyName, value); |
| _requireNotEmpty(propertyName, deprecatedDetails); |
| _deprecatedDetails = deprecatedDetails; |
| } |
| |
| void _setDocumentation(Map<Object?, Object?> data) { |
| const propertyName = 'documentation'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| if (_documentation != null) _alreadySpecified(propertyName); |
| |
| var documentationValue = _requireType<String>(propertyName, value); |
| _requireNotEmpty(propertyName, documentationValue); |
| _documentation = documentationValue; |
| } |
| |
| void _setHasPublishedDocs(Map<Object?, Object?> data) { |
| const propertyName = 'hasPublishedDocs'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| var hasPublishedValue = _requireType<bool>(propertyName, value); |
| _hasPublishedDocs = hasPublishedValue || (_hasPublishedDocs ?? false); |
| } |
| |
| void _setRemovedIn(Map<Object?, Object?> data) { |
| const propertyName = 'removedIn'; |
| if (!data.containsKey(propertyName)) return; |
| |
| var value = data[propertyName]; |
| if (_removedIn != null) _alreadySpecified(propertyName); |
| |
| var removedInValue = _requireType<String>(propertyName, value); |
| if (removedInValue.split('.').length != 2) { |
| _throwLintError( |
| "The '$propertyName' property must be in 'major.minor' format."); |
| } |
| |
| _removedIn = removedInValue; |
| } |
| |
| Never _throwLintError(String message) { |
| throw StateError('$sharedName - $message'); |
| } |
| |
| List<CodeInfo> _validateCodes() { |
| if (_wasRemoved) return const []; |
| |
| if (_codes.isEmpty) { |
| throw StateError('Tried to call build a RuleInfo without a code added!'); |
| } |
| |
| var codeInfos = <CodeInfo>[]; |
| for (var code in _codes) { |
| var problemMessage = code.problemMessage; |
| if (problemMessage == null) { |
| _throwLintError( |
| "'LintCode.${code.uniqueName}' is missing a 'problemMessage'."); |
| } |
| |
| // TODO(parlough): Eventually require that codes have a correction message. |
| // var correctionMessage = code.correctionMessage; |
| // if (code.correctionMessage == null) { |
| // _throwLintError("'LintCode.${code.uniqueName}' is missing a 'correctionMessage'."); |
| // } |
| |
| codeInfos.add(CodeInfo(code.uniqueName, |
| problemMessage: problemMessage, |
| correctionMessage: code.correctionMessage)); |
| } |
| |
| return codeInfos; |
| } |
| } |