[messages] Handle camelCase-formatted files.

Updates the logic in `pkg/analyzer_utilities` and
`pkg/front_end/test/messages_suite.dart` for processing
`messages.yaml` files so that:

- Message keys in analyzer-style `messages.yaml` files can be
  `camelCase`, `lower_snake_case`, or `UPPER_SNAKE_CASE` (previously
  they could only be `lower_snake_case` or `UPPER_SNAKE_CASE`).

- Message keys in CFE-style `messages.yaml` files can be either
  `camelCase` or `PascalCase` (previously they could only be
  `PascalCase`).

- `sharedName` fields can be `camelCase`, `lower_snake_case`, or
  `UPPER_SNAKE_CASE` (previously they could only be `lower_snake_case`
  or `UPPER_SNAKE_CASE`).

- `analyzerCode` fields in `pkg/_fe_analyzer_shared/messages.yaml` can
  be `ClassName.lower_snake_case`, `ClassName.UPPER_SNAKE_CASE`, or
  `camelCase`, where `ClassName` is ignored (previously they could
  only be `ClassName.lower_snake_case` or
  `ClassName.UPPER_SNAKE_CASE`).

This paves the way for a follow-up CL in which all these fields and
keys will be standardized to `camelCase`, and then the ability to
specify them in `lower_snake_case` or `UPPER_SNAKE_CASE` will be
removed. This will eliminate a significant inconsistency between the
diagnostic code formats in the analyzer and the front end.

Note that a few diagnostic names contain an underscore immediately
followed by a digit in their snake case representation:

- `final_not_initialized_constructor_1`
- `final_not_initialized_constructor_2`
- `final_not_initialized_constructor_3_plus`
- `lines_longer_than_80_chars`

When these are converted to `camelCase` form, the code generator will
no longer know to introduce the underscores when converting them back
to `snake_case` form (e.g. `finalNotInitializedConstructor1` will get
converted to `final_not_initialized_constructor1`). The snake case
forms are an important part of the customer facing API (since they are
what is accepted in `// ignore:` comments), so in order to preserve
the existing snake case names, a hardcoded map is introduced,
`_snakeCaseExceptions`.

Change-Id: I6a6a696444ccd92dd6574a7cde08da88ac5a7135
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/466540
Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
Commit-Queue: Paul Berry <paulberry@google.com>
This commit is contained in:
Paul Berry
2025-12-09 11:56:11 -08:00
committed by Commit Queue
parent d0b5024e40
commit e6228e34ca
5 changed files with 88 additions and 18 deletions
@@ -196,7 +196,9 @@ List<M> decodeAnalyzerMessagesYaml<M extends AnalyzerMessage>(
key: keyNode,
value: diagnosticValue,
decoder: (messageYaml) {
var analyzerCode = DiagnosticCodeName(snakeCaseName: diagnosticName);
var analyzerCode = DiagnosticCodeName.fromCamelOrSnakeCase(
diagnosticName,
);
return decodeMessage(
messageYaml,
analyzerCode: analyzerCode,
+77 -13
View File
@@ -153,7 +153,7 @@ abstract class CfeStyleMessage extends Message {
///
/// This is the key corresponding to the diagnostic's entry in
/// `messages.yaml`.
final String frontEndCode;
final DiagnosticCodeName frontEndCode;
CfeStyleMessage(MessageYaml messageYaml)
: cfeSeverity = messageYaml.get(
@@ -172,7 +172,18 @@ abstract class CfeStyleMessage extends Message {
},
ifAbsent: () => null,
),
frontEndCode = messageYaml.keyString,
frontEndCode = switch (messageYaml.keyString) {
var s when s.isCamelCase => DiagnosticCodeName.fromCamelCase(
messageYaml.keyString,
),
var s when s.isPascalCase => DiagnosticCodeName.fromPascalCase(
messageYaml.keyString,
),
_ => throw LocatedError(
'Front end codes must be camelCase or PascalCase',
span: messageYaml.keySpan,
),
},
super(messageYaml, requireProblemMessage: true) {
// Ignore extra keys related to front end example-based tests.
messageYaml.allowExtraKeys({
@@ -211,24 +222,74 @@ sealed class Conversion {
/// This class implements [Comparable], so lists of it can be safely
/// [List.sort]ed.
class DiagnosticCodeName implements Comparable<DiagnosticCodeName> {
/// The diagnostic name.
/// Exceptions to the usual rules for converting diagnostic code names from
/// camel case to snake case.
///
/// The diagnostic name is in "snake case", meaning it consists of words
/// separated by underscores. Those words might be lower case or upper case.
/// Normally, diagnostic code names are converted from camel case to snake
/// case using [StringExtension.toSnakeCase]. But in rare situations when the
/// name contains numbers, this can produce results that aren't ideal. Rather
/// than try to fix these rare situations in a general fashion, it's easier to
/// just have an explicit map of the problematic names, with their preferred
/// snake case forms.
static const Map<String, String> _snakeCaseExceptions = {
'finalNotInitializedConstructor1': 'final_not_initialized_constructor_1',
'finalNotInitializedConstructor2': 'final_not_initialized_constructor_2',
'finalNotInitializedConstructor3Plus':
'final_not_initialized_constructor_3_plus',
'linesLongerThan80Chars': 'lines_longer_than_80_chars',
};
/// The diagnostic name, as a "snake case" name (words separated by
/// underscores).
///
/// The name might be lower case or upper case.
// TODO(paulberry): change `messages.yaml` to consistently use lower snake
// case, and remove [lowerSnakeCaseName].
final String snakeCaseName;
DiagnosticCodeName({required this.snakeCaseName});
/// The diagnostic name, as a "camel case" name (lower case word followed by
/// capitalized words, with no separation between words).
final String camelCaseName;
DiagnosticCodeName.fromCamelCase(this.camelCaseName)
: snakeCaseName =
_snakeCaseExceptions[camelCaseName] ?? camelCaseName.toSnakeCase() {
if (snakeCaseName.toLowerCase() != snakeCaseName) {
throw 'Snake case name ${json.encode(snakeCaseName)} is not all lower '
'case';
}
if (snakeCaseName.toCamelCase() != camelCaseName) {
throw 'Round-trip conversion from ${json.encode(camelCaseName)} to snake '
'case and back produces ${json.encode(snakeCaseName.toCamelCase())}';
}
}
factory DiagnosticCodeName.fromCamelOrSnakeCase(String value) =>
value.isCamelCase
? DiagnosticCodeName.fromCamelCase(value)
: DiagnosticCodeName.fromSnakeCase(value);
factory DiagnosticCodeName.fromPascalCase(String pascalCaseName) {
var snakeCaseName = pascalCaseName.toSnakeCase();
var camelCaseName = snakeCaseName.toPascalCase();
return DiagnosticCodeName._(
snakeCaseName: snakeCaseName,
camelCaseName: camelCaseName,
);
}
DiagnosticCodeName.fromSnakeCase(this.snakeCaseName)
: camelCaseName = snakeCaseName.toCamelCase();
DiagnosticCodeName._({
required this.snakeCaseName,
required this.camelCaseName,
});
/// The string that should be generated into analyzer source code to refer to
/// this diagnostic code.
String get analyzerCodeReference => ['diag', camelCaseName].join('.');
/// The diagnostic name, converted to camel case.
String get camelCaseName => snakeCaseName.toCamelCase();
@override
int get hashCode => snakeCaseName.hashCode;
@@ -410,7 +471,7 @@ class DiagnosticTables {
activeMessagesByPackage = {};
DiagnosticTables._(List<Message> messages) {
var frontEndCodeDuplicateChecker = _DuplicateChecker<String>(
var frontEndCodeDuplicateChecker = _DuplicateChecker<DiagnosticCodeName>(
kind: 'Front end code',
);
var analyzerCodeDuplicateChecker = _DuplicateChecker<DiagnosticCodeName>(
@@ -602,7 +663,7 @@ abstract class Message {
) ??
[],
sharedName = switch (messageYaml.getOptionalString('sharedName')) {
var s? => DiagnosticCodeName(snakeCaseName: s),
var s? => DiagnosticCodeName.fromCamelOrSnakeCase(s),
null => null,
},
removedIn = messageYaml.getOptionalString('removedIn'),
@@ -949,10 +1010,13 @@ class SharedMessage extends CfeStyleMessage with MessageWithAnalyzerCode {
switch (s.split('.')) {
case [_, var snakeCaseName]
when snakeCaseName == snakeCaseName.toUpperCase():
return DiagnosticCodeName(snakeCaseName: snakeCaseName);
return DiagnosticCodeName.fromSnakeCase(snakeCaseName);
case [var camelCaseName] when camelCaseName.isCamelCase:
return DiagnosticCodeName.fromCamelCase(camelCaseName);
}
}
throw 'Analyzer codes must take the form ClassName.DIAGNOSTIC_NAME.';
throw 'Analyzer codes must be either camelCase names or must take the form '
'ClassName.DIAGNOSTIC_NAME.';
}
}
+6 -3
View File
@@ -13,6 +13,7 @@ import 'package:_fe_analyzer_shared/src/messages/diagnostic_message.dart'
getMessageRelatedInformation;
import 'package:_fe_analyzer_shared/src/messages/severity.dart'
show CfeSeverity, severityEnumValues;
import 'package:analyzer_utilities/extensions/string.dart';
import 'package:front_end/src/api_prototype/compiler_options.dart'
show CompilerOptions, parseExperimentalArguments, parseExperimentalFlags;
import 'package:front_end/src/api_prototype/experimental_flags.dart'
@@ -176,9 +177,11 @@ class MessageTestSuite extends ChainContext {
File file = new File.fromUri(uri);
String fileContent = file.readAsStringSync();
YamlMap messages = loadYamlNode(fileContent, sourceUrl: uri) as YamlMap;
for (String name in messages.keys) {
for (String camelCaseName in messages.keys) {
try {
YamlMap messageNode = messages.nodes[name] as YamlMap;
// TODO(paulberry): switch CFE to camelCase conventions.
var name = camelCaseName.toSnakeCase().toPascalCase();
YamlMap messageNode = messages.nodes[camelCaseName] as YamlMap;
dynamic message = messageNode.value;
if (message is String) continue;
@@ -552,7 +555,7 @@ class MessageTestSuite extends ChainContext {
),
);
} catch (e, st) {
Error.throwWithStackTrace('While processing $name: $e', st);
Error.throwWithStackTrace('While processing $camelCaseName: $e', st);
}
}
return result;
@@ -631,6 +631,7 @@ parallax
parameterized
partfoo
party
pascal
pause
paused
pays
@@ -152,7 +152,7 @@ class _TemplateCompiler {
_TemplateCompiler({
required this.message,
required this.pseudoSharedCodeValues,
}) : name = message.frontEndCode,
}) : name = message.frontEndCode.pascalCaseName,
problemMessage = message.problemMessage,
correctionMessage = message.correctionMessage,
severity = message.cfeSeverity,