@@ -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.
283323class 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