Skip to content

Commit 180fbf1

Browse files
stereotype441Commit Queue
authored andcommitted
[messages] Move MessageWithAnalyzerCode into analyzer_messages.dart.
Moves the class `MessageWithAnalyzerCode` from `messages.dart` to `analyzer_messages.dart`, along with related private function `_splitText`. This helps consolidate the code to handle analyzer-specific messages in one place. There is no functional change. Change-Id: I6a6a69642555ebd1a6311c313b16772f68cc357f Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/458583 Commit-Queue: Paul Berry <paulberry@google.com> Reviewed-by: Konstantin Shcheglov <scheglov@google.com>
1 parent cade56e commit 180fbf1

File tree

2 files changed

+220
-221
lines changed

2 files changed

+220
-221
lines changed

pkg/analyzer_utilities/lib/analyzer_messages.dart

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,46 @@ List<AnalyzerMessage> decodeAnalyzerMessagesYaml(
278278
return result;
279279
}
280280

281+
/// Splits [text] on spaces using the given [maxWidth] (and [firstLineWidth] if
282+
/// given).
283+
List<String> _splitText(
284+
String text, {
285+
required int maxWidth,
286+
int? firstLineWidth,
287+
}) {
288+
firstLineWidth ??= maxWidth;
289+
var lines = <String>[];
290+
// The character width to use as a maximum width. This starts as
291+
// [firstLineWidth] but becomes [maxWidth] on every iteration after the first.
292+
var width = firstLineWidth;
293+
var lineMaxEndIndex = width;
294+
var lineStartIndex = 0;
295+
296+
while (true) {
297+
if (lineMaxEndIndex >= text.length) {
298+
lines.add(text.substring(lineStartIndex, text.length));
299+
break;
300+
} else {
301+
var lastSpaceIndex = text.lastIndexOf(' ', lineMaxEndIndex);
302+
if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) {
303+
// No space between [lineStartIndex] and [lineMaxEndIndex]. Get the
304+
// _next_ space.
305+
lastSpaceIndex = text.indexOf(' ', lineMaxEndIndex);
306+
if (lastSpaceIndex == -1) {
307+
// No space at all after [lineStartIndex].
308+
lines.add(text.substring(lineStartIndex));
309+
break;
310+
}
311+
}
312+
lines.add(text.substring(lineStartIndex, lastSpaceIndex + 1));
313+
lineStartIndex = lastSpaceIndex + 1;
314+
width = maxWidth;
315+
}
316+
lineMaxEndIndex = lineStartIndex + maxWidth;
317+
}
318+
return lines;
319+
}
320+
281321
/// An [AnalyzerMessage] which is an alias for another, for incremental
282322
/// deprecation purposes.
283323
class AliasMessage extends AnalyzerMessage {
@@ -354,3 +394,183 @@ class AnalyzerMessage extends Message with MessageWithAnalyzerCode {
354394
}
355395
}
356396
}
397+
398+
/// Interface class for diagnostic messages that have an analyzer code, and thus
399+
/// can be reported by the analyzer.
400+
mixin MessageWithAnalyzerCode on Message {
401+
/// The code used by the analyzer to refer to this diagnostic message.
402+
AnalyzerCode get analyzerCode;
403+
404+
/// The name of the constant in analyzer code that should be used to refer to
405+
/// this message.
406+
String get constantName => analyzerCode.camelCaseName;
407+
408+
/// Whether diagnostics with this code have documentation for them that has
409+
/// been published.
410+
///
411+
/// `null` if the YAML doesn't contain this information.
412+
bool get hasPublishedDocs;
413+
414+
void outputConstantHeader(StringSink out) {
415+
out.write(toAnalyzerComments(indent: ' '));
416+
if (deprecatedMessage != null) {
417+
out.writeln(' @Deprecated("$deprecatedMessage")');
418+
}
419+
}
420+
421+
/// Generates a dart declaration for this diagnostic, suitable for inclusion
422+
/// in the diagnostic class [className].
423+
///
424+
/// [diagnosticCode] is the name of the diagnostic to be generated.
425+
void toAnalyzerCode(
426+
GeneratedDiagnosticClassInfo diagnosticClassInfo, {
427+
String? sharedNameReference,
428+
required MemberAccumulator memberAccumulator,
429+
}) {
430+
var diagnosticCode = analyzerCode.snakeCaseName;
431+
var correctionMessage = this.correctionMessage;
432+
var parameters = this.parameters;
433+
var usesParameters = [problemMessage, correctionMessage].any(
434+
(value) =>
435+
value != null && value.any((part) => part is TemplateParameterPart),
436+
);
437+
String className;
438+
String templateParameters = '';
439+
String? withArgumentsName;
440+
if (parameters.isNotEmpty && !usesParameters) {
441+
throw 'Error code declares parameters using a `parameters` entry, but '
442+
"doesn't use them";
443+
} else if (parameters.values.any((p) => !p.type.isSupportedByAnalyzer)) {
444+
// Do not generate literate API yet.
445+
className = diagnosticClassInfo.name;
446+
} else if (parameters.isNotEmpty) {
447+
// Parameters are present so generate a diagnostic template (with
448+
// `.withArguments` support).
449+
className = diagnosticClassInfo.templateName;
450+
var withArgumentsParams = parameters.entries
451+
.map((p) => 'required ${p.value.type.analyzerName} ${p.key}')
452+
.join(', ');
453+
var argumentNames = parameters.keys.join(', ');
454+
withArgumentsName = '_withArguments${analyzerCode.pascalCaseName}';
455+
templateParameters =
456+
'<LocatableDiagnostic Function({$withArgumentsParams})>';
457+
var newIfNeeded = diagnosticClassInfo.file.shouldUseExplicitNewOrConst
458+
? 'new '
459+
: '';
460+
memberAccumulator.staticMethods[withArgumentsName] =
461+
'''
462+
static LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
463+
return ${newIfNeeded}LocatableDiagnosticImpl(
464+
${diagnosticClassInfo.name}.$constantName, [$argumentNames]);
465+
}''';
466+
} else {
467+
// Parameters are not present so generate a "withoutArguments" constant.
468+
className = diagnosticClassInfo.withoutArgumentsName;
469+
}
470+
471+
var constant = StringBuffer();
472+
outputConstantHeader(constant);
473+
constant.writeln(
474+
' static const $className$templateParameters $constantName =',
475+
);
476+
if (diagnosticClassInfo.file.shouldUseExplicitNewOrConst) {
477+
constant.writeln('const ');
478+
}
479+
constant.writeln('$className(');
480+
constant.writeln(
481+
'${sharedNameReference ?? "'${sharedName ?? diagnosticCode}'"},',
482+
);
483+
var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */;
484+
var messageAsCode = convertTemplate(problemMessage);
485+
var messageLines = _splitText(
486+
messageAsCode,
487+
maxWidth: maxWidth,
488+
firstLineWidth: maxWidth + 4,
489+
);
490+
constant.writeln('${messageLines.map(_encodeString).join('\n')},');
491+
if (correctionMessage != null) {
492+
constant.write('correctionMessage: ');
493+
var code = convertTemplate(correctionMessage);
494+
var codeLines = _splitText(code, maxWidth: maxWidth);
495+
constant.writeln('${codeLines.map(_encodeString).join('\n')},');
496+
}
497+
if (hasPublishedDocs) {
498+
constant.writeln('hasPublishedDocs:true,');
499+
}
500+
if (isUnresolvedIdentifier) {
501+
constant.writeln('isUnresolvedIdentifier:true,');
502+
}
503+
if (sharedName != null) {
504+
constant.writeln("uniqueName: '$diagnosticCode',");
505+
}
506+
if (withArgumentsName != null) {
507+
constant.writeln('withArguments: $withArgumentsName,');
508+
}
509+
constant.writeln('expectedTypes: ${_computeExpectedTypes()},');
510+
constant.writeln(');');
511+
memberAccumulator.constants[constantName] = constant.toString();
512+
513+
if (diagnosticClassInfo.deprecatedSnakeCaseNames.contains(diagnosticCode)) {
514+
memberAccumulator.constants[diagnosticCode] =
515+
'''
516+
@Deprecated("Please use $constantName")
517+
static const ${diagnosticClassInfo.name} $diagnosticCode = $constantName;
518+
''';
519+
}
520+
}
521+
522+
/// Generates doc comments for this error code.
523+
String toAnalyzerComments({String indent = ''}) {
524+
// Start with the comment specified in `messages.yaml`.
525+
var out = StringBuffer();
526+
List<String> commentLines = switch (comment) {
527+
null || '' => [],
528+
var c => c.split('\n'),
529+
};
530+
531+
// Add a `Parameters:` section to the bottom of the comment if appropriate.
532+
switch (parameters) {
533+
case Map(isEmpty: true):
534+
if (commentLines.isNotEmpty) commentLines.add('');
535+
commentLines.add('No parameters.');
536+
default:
537+
if (commentLines.isNotEmpty) commentLines.add('');
538+
commentLines.add('Parameters:');
539+
for (var MapEntry(key: name, value: p) in parameters.entries) {
540+
var prefix = '${p.type.messagesYamlName} $name: ';
541+
var extraIndent = ' ' * prefix.length;
542+
var firstLineWidth = 80 - 4 - indent.length;
543+
var lines = _splitText(
544+
'$prefix${p.comment}',
545+
maxWidth: firstLineWidth - prefix.length,
546+
firstLineWidth: firstLineWidth,
547+
);
548+
commentLines.add(lines[0]);
549+
for (var line in lines.skip(1)) {
550+
commentLines.add('$extraIndent$line');
551+
}
552+
}
553+
}
554+
555+
// Indent the result and prefix with `///`.
556+
for (var line in commentLines) {
557+
out.writeln('$indent///${line.isEmpty ? '' : ' '}$line');
558+
}
559+
return out.toString();
560+
}
561+
562+
String _computeExpectedTypes() {
563+
var expectedTypes = [
564+
for (var parameter in parameters.values)
565+
'ExpectedType.${parameter.type.name}',
566+
];
567+
return '[${expectedTypes.join(', ')}]';
568+
}
569+
570+
String _encodeString(String s) {
571+
// JSON encoding gives us mostly what we need.
572+
var jsonEncoded = json.encode(s);
573+
// But we also need to escape `$`.
574+
return jsonEncoded.replaceAll(r'$', r'\$');
575+
}
576+
}

0 commit comments

Comments
 (0)