| // 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/analysis_rule/rule_state.dart'; |
| import 'package:analyzer_utilities/lint_messages.dart'; |
| import 'package:analyzer_utilities/messages.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| const String _messagesFileName = 'pkg/linter/messages.yaml'; |
| |
| final Map<String, RuleInfo> messagesRuleInfo = () { |
| { |
| var lintNames = lintMessages |
| .map((m) => m.analyzerCode.lowerSnakeCaseName) |
| .toList(growable: false); |
| var lintCodeKeysSorted = lintNames.sorted(); |
| for (var i = 0; i < lintNames.length; i++) { |
| if (lintNames[i] != lintCodeKeysSorted[i]) { |
| throw StateError( |
| "The LintCode entries in '$_messagesFileName' " |
| "are not sorted alphabetically, starting at '${lintNames[i]}'.", |
| ); |
| } |
| } |
| } |
| |
| var builders = <String, _RuleBuilder>{}; |
| for (var message in lintMessages) { |
| var sharedNameString = |
| (message.sharedName ?? message.analyzerCode).lowerSnakeCaseName; |
| var rule = builders.putIfAbsent( |
| sharedNameString, |
| () => _RuleBuilder(sharedNameString), |
| ); |
| rule.addEntry(message.analyzerCode.lowerSnakeCaseName, message); |
| } |
| |
| return builders.map((key, value) { |
| try { |
| return MapEntry(key, value.build()); |
| } catch (e, st) { |
| Error.throwWithStackTrace('Problem with lint code $key: $e', st); |
| } |
| }); |
| }(); |
| |
| class CodeInfo { |
| final String uniqueName; |
| final List<TemplatePart> problemMessage; |
| final List<TemplatePart>? correctionMessage; |
| |
| CodeInfo( |
| this.uniqueName, { |
| required this.problemMessage, |
| this.correctionMessage, |
| }); |
| } |
| |
| class RuleInfo { |
| final String name; |
| final List<CodeInfo> codes; |
| final List<RuleState> states; |
| final Set<LintCategory> categories; |
| final bool hasPublishedDocs; |
| final String? documentation; |
| final String deprecatedDetails; |
| final bool removed; |
| |
| RuleInfo({ |
| required this.name, |
| required this.codes, |
| required this.categories, |
| required this.hasPublishedDocs, |
| required this.documentation, |
| required this.deprecatedDetails, |
| required this.states, |
| required this.removed, |
| }); |
| } |
| |
| // 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, |
| List<TemplatePart> problemMessage, |
| List<TemplatePart>? correctionMessage, |
| }) |
| > |
| _codes = []; |
| Map<LintStateName, Version>? _states; |
| Set<LintCategory>? _categories; |
| bool? _hasPublishedDocs; |
| String? _documentation; |
| String? _deprecatedDetails; |
| |
| _RuleBuilder(this.sharedName); |
| |
| bool get _wasRemoved => |
| _states?.keys.any((key) => key == LintStateName.removed) ?? false; |
| |
| void addEntry(String uniqueName, LintMessage message) { |
| _addCode(uniqueName, message); |
| |
| _setStates(message); |
| _setCategories(message); |
| _setDeprecatedDetails(message); |
| _setDocumentation(message); |
| _setHasPublishedDocs(message); |
| } |
| |
| RuleInfo build() => RuleInfo( |
| name: sharedName, |
| codes: _validateCodes(), |
| states: _validateStates(), |
| categories: _requireSpecified( |
| 'categories', |
| _categories, |
| ifNotRemovedFallback: const {}, |
| ), |
| hasPublishedDocs: _hasPublishedDocs ?? false, |
| documentation: _documentation, |
| deprecatedDetails: _requireSpecified( |
| 'deprecatedDetails', |
| _deprecatedDetails, |
| ), |
| removed: _wasRemoved, |
| ); |
| |
| void _addCode(String name, LintMessage message) { |
| if (_codes.map((code) => code.uniqueName).any((n) => n == name)) { |
| _throwLintError( |
| "Has more than one LintCode with '$name' as its 'uniqueName'.", |
| ); |
| } |
| |
| _codes.add(( |
| uniqueName: name, |
| problemMessage: message.problemMessage, |
| correctionMessage: message.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; |
| } |
| |
| void _setCategories(LintMessage message) { |
| const propertyName = 'categories'; |
| var value = message.categories; |
| if (value == null) return; |
| |
| if (_categories != null) _alreadySpecified(propertyName); |
| _categories = value; |
| } |
| |
| void _setDeprecatedDetails(LintMessage message) { |
| const propertyName = 'deprecatedDetails'; |
| var value = message.deprecatedDetails; |
| if (value == null) return; |
| |
| if (_deprecatedDetails != null) _alreadySpecified(propertyName); |
| |
| _requireNotEmpty(propertyName, value); |
| _deprecatedDetails = value; |
| } |
| |
| void _setDocumentation(LintMessage message) { |
| const propertyName = 'documentation'; |
| var value = message.documentation; |
| if (value == null) return; |
| |
| if (_documentation != null) _alreadySpecified(propertyName); |
| |
| _requireNotEmpty(propertyName, value); |
| _documentation = value; |
| } |
| |
| void _setHasPublishedDocs(LintMessage message) { |
| var value = message.hasPublishedDocs; |
| |
| _hasPublishedDocs = value || (_hasPublishedDocs ?? false); |
| } |
| |
| void _setStates(LintMessage message) { |
| const propertyName = 'state'; |
| var value = message.state; |
| if (value == null) return; |
| |
| if (_states != null) _alreadySpecified(propertyName); |
| |
| _states = value; |
| } |
| |
| 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.isEmpty) { |
| _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; |
| } |
| |
| List<RuleState> _validateStates() { |
| var states = _states; |
| if (states == null || states.isEmpty) { |
| throw StateError('Tried to build a RuleInfo without a state added!'); |
| } |
| |
| var sortedStates = states.entries |
| .map( |
| (entry) => switch (entry.key) { |
| LintStateName.experimental => RuleState.experimental( |
| since: entry.value, |
| ), |
| LintStateName.stable => RuleState.stable(since: entry.value), |
| LintStateName.internal => RuleState.internal(since: entry.value), |
| LintStateName.deprecated => RuleState.deprecated( |
| since: entry.value, |
| ), |
| // Note: the reason `RuleState.removed` is deprecated is to |
| // encourage clients to use `AbstractAnalysisRule`, so this |
| // reference is ok. |
| // ignore: deprecated_member_use |
| LintStateName.removed => RuleState.removed(since: entry.value), |
| }, |
| ) |
| .sortedBy<VersionRange>((state) => state.since ?? Version.none); |
| |
| return sortedStates; |
| } |
| } |