blob: f6c6455bd499e38fe59e8d4d9effeaff27f6b537 [file] [log] [blame]
// 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;
}
}