blob: 7856f34b4a1b8e4d56d4ddee2afb971b11127cf5 [file] [log] [blame]
// Copyright (c) 2025, 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.
/// @docImport 'package:_fe_analyzer_shared/src/base/errors.dart';
library;
import 'dart:convert';
import 'dart:io';
import 'package:analyzer_testing/package_root.dart' as pkg_root;
import 'package:analyzer_utilities/analyzer_message_constant_style.dart';
import 'package:analyzer_utilities/extensions/string.dart';
import 'package:analyzer_utilities/messages.dart';
import 'package:analyzer_utilities/tools.dart';
import 'package:path/path.dart';
import 'package:yaml/yaml.dart' show YamlMap, YamlScalar, loadYamlNode;
/// Base diagnostic classes used for analyzer messages.
const analyzerBaseClasses = DiagnosticBaseClasses(
requiresTypeArgument: true,
withArgumentsClass: 'DiagnosticWithArguments',
withExpectedTypesClass: 'DiagnosticCodeWithExpectedTypes',
withoutArgumentsClass: 'DiagnosticWithoutArguments',
withoutArgumentsImplClass: 'DiagnosticWithoutArgumentsImpl',
);
const codesFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/error/codes.g.dart',
parentLibrary: 'package:analyzer/src/error/codes.dart',
);
/// Information about all the classes derived from `DiagnosticCode` that are
/// code-generated based on the contents of the analyzer and front end
/// `messages.yaml` files.
///
/// Note: to look up an error class by name, use [DiagnosticClassInfo.byName].
const List<DiagnosticClassInfo> diagnosticClasses = [
linterLintCodeInfo,
DiagnosticClassInfo(
file: optionCodesFile,
name: 'AnalysisOptionsErrorCode',
type: AnalyzerDiagnosticType.compileTimeError,
),
DiagnosticClassInfo(
file: optionCodesFile,
name: 'AnalysisOptionsWarningCode',
type: AnalyzerDiagnosticType.staticWarning,
),
DiagnosticClassInfo(
file: codesFile,
name: 'CompileTimeErrorCode',
type: AnalyzerDiagnosticType.compileTimeError,
),
DiagnosticClassInfo(
file: syntacticErrorsFile,
name: 'ScannerErrorCode',
type: AnalyzerDiagnosticType.syntacticError,
),
DiagnosticClassInfo(
file: codesFile,
name: 'StaticWarningCode',
type: AnalyzerDiagnosticType.staticWarning,
),
DiagnosticClassInfo(
file: codesFile,
name: 'WarningCode',
type: AnalyzerDiagnosticType.staticWarning,
),
DiagnosticClassInfo(
file: ffiCodesFile,
name: 'FfiCode',
type: AnalyzerDiagnosticType.compileTimeError,
),
DiagnosticClassInfo(
file: hintCodesFile,
name: 'HintCode',
type: AnalyzerDiagnosticType.hint,
),
DiagnosticClassInfo(
file: syntacticErrorsFile,
name: 'ParserErrorCode',
type: AnalyzerDiagnosticType.syntacticError,
),
DiagnosticClassInfo(
file: manifestWarningCodeFile,
name: 'ManifestWarningCode',
type: AnalyzerDiagnosticType.staticWarning,
),
DiagnosticClassInfo(
file: pubspecWarningCodeFile,
name: 'PubspecWarningCode',
type: AnalyzerDiagnosticType.staticWarning,
),
DiagnosticClassInfo(
file: todoCodesFile,
name: 'TodoCode',
type: AnalyzerDiagnosticType.todo,
comment: '''
The error code indicating a marker in code for work that needs to be finished
or revisited.
''',
),
DiagnosticClassInfo(
file: transformSetErrorCodeFile,
name: 'TransformSetErrorCode',
type: AnalyzerDiagnosticType.compileTimeError,
comment: '''
An error code representing a problem in a file containing an encoding of a
transform set.
''',
),
];
const ffiCodesFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/dart/error/ffi_code.g.dart',
parentLibrary: 'package:analyzer/src/dart/error/ffi_code.dart',
);
const String generatedLintCodesPath = 'linter/lib/src/lint_codes.g.dart';
const hintCodesFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/dart/error/hint_codes.g.dart',
parentLibrary: 'package:analyzer/src/dart/error/hint_codes.dart',
);
const lintCodesFile = GeneratedDiagnosticFile(
path: generatedLintCodesPath,
parentLibrary: 'package:linter/src/lint_codes.dart',
package: AnalyzerDiagnosticPackage.linter,
);
/// Base diagnostic classes used for lint messages.
const linterBaseClasses = DiagnosticBaseClasses(
requiresTypeArgument: false,
withArgumentsClass: 'LinterLintTemplate',
withExpectedTypesClass: 'LinterLintCode',
withoutArgumentsClass: 'LinterLintWithoutArguments',
withoutArgumentsImplClass: 'LinterLintWithoutArguments',
);
const linterLintCodeInfo = DiagnosticClassInfo(
file: lintCodesFile,
name: 'LinterLintCode',
type: AnalyzerDiagnosticType.lint,
);
const manifestWarningCodeFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/manifest/manifest_warning_code.g.dart',
parentLibrary: 'package:analyzer/src/manifest/manifest_warning_code.dart',
);
const optionCodesFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/analysis_options/error/option_codes.g.dart',
parentLibrary:
'package:analyzer/src/analysis_options/error/option_codes.dart',
);
const pubspecWarningCodeFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/pubspec/pubspec_warning_code.g.dart',
parentLibrary: 'package:analyzer/src/pubspec/pubspec_warning_code.dart',
);
const syntacticErrorsFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/dart/error/syntactic_errors.g.dart',
parentLibrary: 'package:analyzer/src/dart/error/syntactic_errors.dart',
);
const todoCodesFile = GeneratedDiagnosticFile(
path: 'analyzer/lib/src/dart/error/todo_codes.g.dart',
parentLibrary: 'package:analyzer/src/dart/error/todo_codes.dart',
);
const transformSetErrorCodeFile = GeneratedDiagnosticFile(
path:
'analysis_server/lib/src/services/correction/fix/data_driven/'
'transform_set_error_code.g.dart',
parentLibrary:
'package:analysis_server/src/services/correction/fix/data_driven/'
'transform_set_error_code.dart',
package: AnalyzerDiagnosticPackage.analysisServer,
);
/// Decoded messages from the analysis server's `messages.yaml` file.
final List<AnalyzerMessage> analysisServerMessages = decodeAnalyzerMessagesYaml(
analysisServerPkgPath,
package: AnalyzerDiagnosticPackage.analysisServer,
);
/// The path to the `analysis_server` package.
final String analysisServerPkgPath = normalize(
join(pkg_root.packageRoot, 'analysis_server'),
);
/// Decoded messages from the analyzer's `messages.yaml` file.
final List<AnalyzerMessage> analyzerMessages = decodeAnalyzerMessagesYaml(
analyzerPkgPath,
package: AnalyzerDiagnosticPackage.analyzer,
);
/// The path to the `analyzer` package.
final String analyzerPkgPath = normalize(
join(pkg_root.packageRoot, 'analyzer'),
);
/// The path to the `linter` package.
final String linterPkgPath = normalize(join(pkg_root.packageRoot, 'linter'));
/// Decoded messages from the linter's `messages.yaml` file.
final List<AnalyzerMessage> lintMessages = decodeAnalyzerMessagesYaml(
linterPkgPath,
allowLinterKeys: true,
package: AnalyzerDiagnosticPackage.linter,
);
/// Decodes a YAML object (in analyzer style `messages.yaml` format) into a list
/// of [AnalyzerMessage]s.
///
/// If [allowLinterKeys], error checking logic will not reject key/value pairs
/// that are used by the linter.
List<AnalyzerMessage> decodeAnalyzerMessagesYaml(
String packagePath, {
bool allowLinterKeys = false,
required AnalyzerDiagnosticPackage package,
}) {
var path = join(packagePath, 'messages.yaml');
var yaml = loadYamlNode(
File(path).readAsStringSync(),
sourceUrl: Uri.file(path),
);
var result = <AnalyzerMessage>[];
if (yaml is! YamlMap) {
throw LocatedError('root node is not a map', span: yaml.span);
}
for (var classEntry in yaml.nodes.entries) {
var keyNode = classEntry.key as YamlScalar;
var className = keyNode.value;
if (className is! String) {
throw LocatedError(
'non-string class key ${json.encode(className)}',
span: keyNode.span,
);
}
var classValue = classEntry.value;
if (classValue is! YamlMap) {
throw LocatedError(
'value associated with class key $className is not a map',
span: classValue.span,
);
}
for (var diagnosticEntry in classValue.nodes.entries) {
var keyNode = diagnosticEntry.key as YamlScalar;
var diagnosticName = keyNode.value;
if (diagnosticName is! String) {
throw LocatedError(
'non-string diagnostic key ${json.encode(diagnosticName)}',
span: keyNode.span,
);
}
var diagnosticValue = diagnosticEntry.value;
if (diagnosticValue is! YamlMap) {
throw LocatedError(
'value associated with diagnostic is not a map',
span: diagnosticValue.span,
);
}
AnalyzerMessage message = MessageYaml.decode(
key: keyNode,
value: diagnosticValue,
decoder: (messageYaml) {
var analyzerCode = AnalyzerCode(
diagnosticClass: DiagnosticClassInfo.byName(className),
snakeCaseName: diagnosticName,
);
return AnalyzerMessage(
messageYaml,
analyzerCode: analyzerCode,
allowLinterKeys: allowLinterKeys,
package: package,
);
},
);
result.add(message);
if (message case AliasMessage(:var aliasFor)) {
var aliasForPath = aliasFor.split('.');
if (aliasForPath.isEmpty) {
throw LocatedError(
"The 'aliasFor' value is empty",
span: diagnosticValue.span,
);
}
var node = yaml;
for (var key in aliasForPath) {
var value = node[key];
if (value is! YamlMap) {
throw LocatedError(
'No Map value at "$aliasFor"',
span: diagnosticValue.span,
);
}
node = value;
}
}
}
}
return result;
}
/// Splits [text] on spaces using the given [maxWidth] (and [firstLineWidth] if
/// given).
List<String> _splitText(
String text, {
required int maxWidth,
int? firstLineWidth,
}) {
firstLineWidth ??= maxWidth;
var lines = <String>[];
// The character width to use as a maximum width. This starts as
// [firstLineWidth] but becomes [maxWidth] on every iteration after the first.
var width = firstLineWidth;
var lineMaxEndIndex = width;
var lineStartIndex = 0;
while (true) {
if (lineMaxEndIndex >= text.length) {
lines.add(text.substring(lineStartIndex, text.length));
break;
} else {
var lastSpaceIndex = text.lastIndexOf(' ', lineMaxEndIndex);
if (lastSpaceIndex == -1 || lastSpaceIndex <= lineStartIndex) {
// No space between [lineStartIndex] and [lineMaxEndIndex]. Get the
// _next_ space.
lastSpaceIndex = text.indexOf(' ', lineMaxEndIndex);
if (lastSpaceIndex == -1) {
// No space at all after [lineStartIndex].
lines.add(text.substring(lineStartIndex));
break;
}
}
lines.add(text.substring(lineStartIndex, lastSpaceIndex + 1));
lineStartIndex = lastSpaceIndex + 1;
width = maxWidth;
}
lineMaxEndIndex = lineStartIndex + maxWidth;
}
return lines;
}
/// An [AnalyzerMessage] which is an alias for another, for incremental
/// deprecation purposes.
class AliasMessage extends AnalyzerMessage {
String aliasFor;
AliasMessage(
super.messageYaml, {
required this.aliasFor,
required super.analyzerCode,
required super.allowLinterKeys,
required super.package,
}) : super._();
String get aliasForClass => aliasFor.split('.').first;
@override
void toAnalyzerCode({
String? sharedNameReference,
required MemberAccumulator memberAccumulator,
}) {
var constant = StringBuffer();
outputConstantHeader(constant);
constant.writeln('const $aliasForClass $constantName =');
constant.writeln('$aliasFor;');
memberAccumulator.constants[constantName] = constant.toString();
}
}
/// Enum representing the packages into which analyzer diagnostics can be
/// generated.
enum AnalyzerDiagnosticPackage {
analyzer(
diagnosticPathPart: 'src/diagnostic/diagnostic',
dirName: 'analyzer',
),
analysisServer(
diagnosticPathPart: 'src/diagnostic',
dirName: 'analysis_server',
shouldIgnorePreferSingleQuotes: true,
),
linter(
diagnosticPathPart: 'src/diagnostic',
dirName: 'linter',
shouldIgnorePreferExpressionFunctionBodies: true,
shouldIgnorePreferSingleQuotes: true,
);
/// The name of the subdirectory of `pkg` containing this package.
final String dirName;
/// The part of the path to the generated `diagnostic.g.dart` file that
/// follows the package's `lib` directory and precedes `diagnostic.g.dart`.
///
/// For example, if [dirName] is `linter` and [diagnosticPathPart] is
/// `src/diagnostic`, then the full path to the generated `diagnostic.g.dart`
/// file will be `pkg/linter/lib/src/diagnostic/diagnostic.g.dart`.
final String diagnosticPathPart;
/// Whether code generated in this package needs an "ignore" comment to ignore
/// the `prefer_expression_function_bodies` lint.
final bool shouldIgnorePreferExpressionFunctionBodies;
/// Whether code generated in this package needs an "ignore" comment to ignore
/// the `prefer_single_quotes` lint.
final bool shouldIgnorePreferSingleQuotes;
const AnalyzerDiagnosticPackage({
required this.diagnosticPathPart,
required this.dirName,
this.shouldIgnorePreferExpressionFunctionBodies = false,
this.shouldIgnorePreferSingleQuotes = false,
});
void writeIgnoresTo(StringBuffer out) {
if (shouldIgnorePreferExpressionFunctionBodies) {
out.write('''
// Code generation is easier if we don't have to decide whether to generate an
// expression function body or a block function body.
// ignore_for_file: prefer_expression_function_bodies
''');
}
if (shouldIgnorePreferSingleQuotes) {
out.write('''
// Code generation is easier using double quotes (since we can use json.convert
// to quote strings).
// ignore_for_file: prefer_single_quotes
''');
}
out.write('''
// Generated comments don't quite align with flutter style.
// ignore_for_file: flutter_style_todos
''');
}
}
/// Enum representing the possible values for the [DiagnosticType] class.
///
/// Code generation logic uses this enum rather than [DiagnosticType] to avoid
/// introducing dependencies between the code generator and the generated code.
enum AnalyzerDiagnosticType {
compileTimeError,
hint,
lint(baseClasses: linterBaseClasses),
staticWarning,
syntacticError,
todo;
/// Base classes used for messages of this type.
final DiagnosticBaseClasses baseClasses;
const AnalyzerDiagnosticType({this.baseClasses = analyzerBaseClasses});
/// The representation of this type in analyzer source code.
String get code => 'DiagnosticType.${name.toSnakeCase().toUpperCase()}';
}
/// In-memory representation of diagnostic information obtained from the
/// analyzer's `messages.yaml` file.
class AnalyzerMessage extends Message with MessageWithAnalyzerCode {
@override
final AnalyzerCode analyzerCode;
@override
final bool hasPublishedDocs;
@override
final AnalyzerDiagnosticPackage package;
factory AnalyzerMessage(
MessageYaml messageYaml, {
required AnalyzerCode analyzerCode,
required bool allowLinterKeys,
required AnalyzerDiagnosticPackage package,
}) {
if (messageYaml.getOptionalString('aliasFor') case var aliasFor?) {
return AliasMessage(
messageYaml,
aliasFor: aliasFor,
analyzerCode: analyzerCode,
allowLinterKeys: allowLinterKeys,
package: package,
);
} else {
return AnalyzerMessage._(
messageYaml,
analyzerCode: analyzerCode,
allowLinterKeys: allowLinterKeys,
package: package,
);
}
}
AnalyzerMessage._(
MessageYaml messageYaml, {
required this.analyzerCode,
required bool allowLinterKeys,
required this.package,
}) : hasPublishedDocs = messageYaml.getBool('hasPublishedDocs'),
super(messageYaml) {
// Ignore extra keys related to analyzer example-based tests.
messageYaml.allowExtraKeys({'experiment'});
if (allowLinterKeys) {
// Ignore extra keys understood by the linter.
messageYaml.allowExtraKeys({'categories', 'deprecatedDetails', 'state'});
}
}
}
/// Description of the set of base messages classes used for a certain message
/// type.
class DiagnosticBaseClasses {
/// Whether the constructor argument `type` must be passed to constructors
/// when constructing messages of this type.
final bool requiresTypeArgument;
/// The name of the concrete class used for messages of this type that require
/// arguments.
final String withArgumentsClass;
/// The name of the concrete class used for messages of this type that require
/// arguments but don't yet support the literate API.
// TODO(paulberry): finish supporting the literate API in all analyzer
// messages and eliminate this.
final String withExpectedTypesClass;
/// The name of the abstract class used for messages of this type that do not
/// require arguments.
final String withoutArgumentsClass;
/// The name of the concrete class used for messages of this type that do not
/// require arguments.
final String withoutArgumentsImplClass;
const DiagnosticBaseClasses({
required this.requiresTypeArgument,
required this.withArgumentsClass,
required this.withExpectedTypesClass,
required this.withoutArgumentsClass,
required this.withoutArgumentsImplClass,
});
}
/// Information about a class derived from `DiagnosticCode`.
class DiagnosticClassInfo {
static final Map<String, DiagnosticClassInfo> _diagnosticClassesByName = () {
var result = <String, DiagnosticClassInfo>{};
for (var info in diagnosticClasses) {
if (result.containsKey(info.name)) {
throw 'Duplicate diagnostic class name: ${json.encode(info.name)}';
}
result[info.name] = info;
}
return result;
}();
static String get _allDiagnosticClassNames =>
(_diagnosticClassesByName.keys.toList()..sort())
.map(json.encode)
.join(', ');
/// The name of this class.
final String name;
/// The generated file containing this class.
final GeneratedDiagnosticFile file;
/// The type of diagnostics in this class.
final AnalyzerDiagnosticType type;
/// Documentation comment to generate for the diagnostic class.
///
/// If no documentation comment is needed, this should be the empty string.
final String comment;
const DiagnosticClassInfo({
required this.file,
required this.name,
required this.type,
this.comment = '',
});
static DiagnosticClassInfo byName(String name) =>
_diagnosticClassesByName[name] ??
(throw 'No diagnostic class named ${json.encode(name)}. Possible names: '
'$_allDiagnosticClassNames');
}
/// Interface class for diagnostic messages that have an analyzer code, and thus
/// can be reported by the analyzer.
mixin MessageWithAnalyzerCode on Message {
late ConstantStyle constantStyle = () {
var usesParameters = [problemMessage, correctionMessage].any(
(value) =>
value != null && value.any((part) => part is TemplateParameterPart),
);
var baseClasses = analyzerCode.diagnosticClass.type.baseClasses;
if (parameters.isNotEmpty && !usesParameters) {
throw 'Error code declares parameters using a `parameters` entry, but '
"doesn't use them";
} else if (parameters.values.any((p) => !p.type.isSupportedByAnalyzer)) {
// Do not generate literate API yet.
return OldConstantStyle(
concreteClassName: baseClasses.withExpectedTypesClass,
staticType: 'DiagnosticCode',
);
} else if (parameters.isNotEmpty) {
// Parameters are present so generate a diagnostic template (with
// `.withArguments` support).
var withArgumentsParams = parameters.entries
.map((p) => 'required ${p.value.type.analyzerName} ${p.key}')
.join(', ');
var templateParameters =
'<LocatableDiagnostic Function({$withArgumentsParams})>';
return WithArgumentsConstantStyle(
concreteClassName: baseClasses.withArgumentsClass,
staticType: 'DiagnosticWithArguments$templateParameters',
withArgumentsParams: withArgumentsParams,
);
} else {
return WithoutArgumentsConstantStyle(
concreteClassName: baseClasses.withoutArgumentsImplClass,
staticType: baseClasses.withoutArgumentsClass,
);
}
}();
late final DiagnosticClassInfo diagnosticClassInfo =
analyzerCode.diagnosticClass;
/// The code used by the analyzer to refer to this diagnostic message.
AnalyzerCode get analyzerCode;
/// The name of the constant in analyzer code that should be used to refer to
/// this message.
String get constantName => analyzerCode.camelCaseName;
/// Whether diagnostics with this code have documentation for them that has
/// been published.
///
/// `null` if the YAML doesn't contain this information.
bool get hasPublishedDocs;
/// The package into which this error code will be generated.
AnalyzerDiagnosticPackage get package;
void outputConstantHeader(StringSink out) {
out.write(toAnalyzerComments(indent: ' '));
if (deprecatedMessage != null) {
out.writeln(' @Deprecated("$deprecatedMessage")');
}
}
/// Generates a dart declaration for this diagnostic, suitable for inclusion
/// in the diagnostic class [className].
///
/// [diagnosticCode] is the name of the diagnostic to be generated.
void toAnalyzerCode({
String? sharedNameReference,
required MemberAccumulator memberAccumulator,
}) {
var diagnosticCode = analyzerCode.snakeCaseName;
var correctionMessage = this.correctionMessage;
String? withArgumentsName;
var baseClasses = analyzerCode.diagnosticClass.type.baseClasses;
var ConstantStyle(:concreteClassName, :staticType) = constantStyle;
if (constantStyle case WithArgumentsConstantStyle(
:var withArgumentsParams,
)) {
var argumentNames = parameters.keys.join(', ');
withArgumentsName = '_withArguments${analyzerCode.pascalCaseName}';
memberAccumulator.staticMethods[withArgumentsName] =
'''
LocatableDiagnostic $withArgumentsName({$withArgumentsParams}) {
return LocatableDiagnosticImpl(
${analyzerCode.analyzerCodeReference}, [$argumentNames]);
}''';
}
var constant = StringBuffer();
outputConstantHeader(constant);
constant.writeln('const $staticType $constantName =');
constant.writeln('$concreteClassName(');
constant.writeln(
'name: ${sharedNameReference ?? "'${sharedName ?? diagnosticCode}'"},',
);
var maxWidth = 80 - 8 /* indentation */ - 2 /* quotes */ - 1 /* comma */;
var messageAsCode = convertTemplate(problemMessage);
var messageLines = _splitText(
messageAsCode,
maxWidth: maxWidth,
firstLineWidth: maxWidth + 4,
);
constant.writeln(
'problemMessage: ${messageLines.map(_encodeString).join('\n')},',
);
if (correctionMessage != null) {
constant.write('correctionMessage: ');
var code = convertTemplate(correctionMessage);
var codeLines = _splitText(code, maxWidth: maxWidth);
constant.writeln('${codeLines.map(_encodeString).join('\n')},');
}
if (hasPublishedDocs) {
constant.writeln('hasPublishedDocs:true,');
}
if (isUnresolvedIdentifier) {
constant.writeln('isUnresolvedIdentifier:true,');
}
if (baseClasses.requiresTypeArgument) {
constant.writeln('type: ${diagnosticClassInfo.type.code},');
}
String uniqueName = analyzerCode.toString().replaceFirst(
'LinterLintCode.',
'LintCode.',
);
constant.writeln("uniqueName: '$uniqueName',");
if (withArgumentsName != null) {
constant.writeln('withArguments: $withArgumentsName,');
}
constant.writeln('expectedTypes: ${_computeExpectedTypes()},');
constant.writeln(');');
memberAccumulator.constants[constantName] = constant.toString();
}
/// Generates doc comments for this error code.
String toAnalyzerComments({String indent = ''}) {
// Start with the comment specified in `messages.yaml`.
var out = StringBuffer();
List<String> commentLines = switch (comment) {
null || '' => [],
var c => c.split('\n'),
};
// Add a `Parameters:` section to the bottom of the comment if appropriate.
switch (parameters) {
case Map(isEmpty: true):
if (commentLines.isNotEmpty) commentLines.add('');
commentLines.add('No parameters.');
default:
if (commentLines.isNotEmpty) commentLines.add('');
commentLines.add('Parameters:');
for (var MapEntry(key: name, value: p) in parameters.entries) {
var prefix = '${p.type.messagesYamlName} $name: ';
var extraIndent = ' ' * prefix.length;
var firstLineWidth = 80 - 4 - indent.length;
var lines = _splitText(
'$prefix${p.comment}',
maxWidth: firstLineWidth - prefix.length,
firstLineWidth: firstLineWidth,
);
commentLines.add(lines[0]);
for (var line in lines.skip(1)) {
commentLines.add('$extraIndent$line');
}
}
}
// Indent the result and prefix with `///`.
for (var line in commentLines) {
out.writeln('$indent///${line.isEmpty ? '' : ' '}$line');
}
return out.toString();
}
/// Generates a dart declaration for this diagnostic, suitable for inclusion
/// in the diagnostic class [className].
///
/// The generated code simply redirects to the primary definition of the
/// diagnostic, imported from `diagnostic.g.dart` using the import prefix
/// `diag`.
void toAnalyzerRedirectCode({required MemberAccumulator memberAccumulator}) {
var ConstantStyle(:staticType) = constantStyle;
var constant = StringBuffer();
outputConstantHeader(constant);
constant.writeln(' static const $staticType $constantName =');
constant.writeln(' diag.$constantName;');
memberAccumulator.constants[constantName] = constant.toString();
}
/// Generates the appropriate declaration for this diagnostic to include in
/// the diagnostic class.
void toClassMember({
String? sharedNameReference,
required MemberAccumulator memberAccumulator,
}) {
toAnalyzerRedirectCode(memberAccumulator: memberAccumulator);
}
String _computeExpectedTypes() {
var expectedTypes = [
for (var parameter in parameters.values)
'ExpectedType.${parameter.type.name}',
];
return '[${expectedTypes.join(', ')}]';
}
String _encodeString(String s) {
// JSON encoding gives us mostly what we need.
var jsonEncoded = json.encode(s);
// But we also need to escape `$`.
return jsonEncoded.replaceAll(r'$', r'\$');
}
}