blob: c85ae571ca854a970c288037ef9e0eaeec425a8a [file] [log] [blame]
// Copyright (c) 2013, 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.
part of '../protoc.dart';
final RegExp _dartIdentifier = RegExp(r'^\w+$');
const String _asyncImportUrl = 'dart:async';
const String _convertImportPrefix = r'$convert';
const String _convertImportUrl = 'dart:convert';
const String _coreImportUrl = 'dart:core';
const String _grpcImportUrl = 'package:grpc/service_api.dart';
const String _protobufImportUrl = 'package:protobuf/protobuf.dart';
const String _typedDataImportPrefix = r'$typed_data';
const String _typedDataImportUrl = 'dart:typed_data';
/// Generates the Dart output files for one .proto input file.
///
/// Outputs include .pb.dart, pbenum.dart, and .pbjson.dart.
class FileGenerator extends ProtobufContainer {
final FileDescriptorProto descriptor;
final GenerationOptions options;
// The relative path used to import the .proto file, as a URI.
final Uri protoFileUri;
final enumGenerators = <EnumGenerator>[];
final messageGenerators = <MessageGenerator>[];
final extensionGenerators = <ExtensionGenerator>[];
final clientApiGenerators = <ClientApiGenerator>[];
final serviceGenerators = <ServiceGenerator>[];
final grpcGenerators = <GrpcServiceGenerator>[];
/// Used to avoid collisions after names have been mangled to match the Dart
/// style.
final Set<String> usedTopLevelNames = <String>{...forbiddenTopLevelNames};
/// Used to avoid collisions in the service file after names have been mangled
/// to match the dart style.
final Set<String> usedTopLevelServiceNames = <String>{
...forbiddenTopLevelNames,
};
final Set<String> usedExtensionNames = <String>{...forbiddenExtensionNames};
/// Whether cross-references have been resolved.
bool _linked = false;
final Edition edition;
@override
final FeatureSet features;
/// Maps imports in the current file to their import prefixes.
/// E.g. in `import 'x/y/z.pb.dart' as $1` this maps `x/y/z.pb.dart` to `$1`.
final Map<String, String> _importPrefixes = {};
/// Get the import prefix of `container` in the current file generator.
///
/// Note that just calling this does not import the `container` in the current
/// file. This just assigns an prefix to the container in the current file
/// generator.
String importPrefix(ProtobufContainer container) {
final protoFilePath = container.fileGen!.protoFileUri.toString();
return _importPrefixes.putIfAbsent(
protoFilePath,
() => '\$${_importPrefixes.length}',
);
}
FileGenerator(
FeatureSetDefaults editionDefaults,
this.descriptor,
this.options,
) : protoFileUri = Uri.file(descriptor.name),
edition = _getEdition(descriptor),
features = resolveFeatures(
_getEditionDefaults(editionDefaults, _getEdition(descriptor)),
descriptor.options.features,
) {
if (protoFileUri.isAbsolute) {
// protoc should never generate an import with an absolute path.
throw 'FAILURE: Import with absolute path is not supported';
}
final declaredMixins = _getDeclaredMixins(descriptor);
final defaultMixinName =
descriptor.options.getExtension(Dart_options.defaultMixin) as String? ??
'';
final defaultMixin =
declaredMixins[defaultMixinName] ?? findMixin(defaultMixinName);
if (defaultMixin == null && defaultMixinName.isNotEmpty) {
throw 'Option default_mixin on file ${descriptor.name}: Unknown mixin '
'$defaultMixinName';
}
// Load and register all enum and message types.
for (var i = 0; i < descriptor.enumType.length; i++) {
enumGenerators.add(
EnumGenerator.topLevel(
descriptor.enumType[i],
this,
usedTopLevelNames,
i,
),
);
}
for (var i = 0; i < descriptor.messageType.length; i++) {
messageGenerators.add(
MessageGenerator.topLevel(
descriptor.messageType[i],
this,
declaredMixins,
defaultMixin,
usedTopLevelNames,
i,
),
);
}
for (var i = 0; i < descriptor.extension.length; i++) {
extensionGenerators.add(
ExtensionGenerator.topLevel(
descriptor.extension[i],
this,
usedExtensionNames,
i,
),
);
}
for (var i = 0; i < descriptor.service.length; i++) {
final service = descriptor.service[i];
if (options.useGrpc) {
grpcGenerators.add(GrpcServiceGenerator(service, this, i));
} else {
final serviceGen = ServiceGenerator(
service,
this,
usedTopLevelServiceNames,
);
serviceGenerators.add(serviceGen);
clientApiGenerators.add(
ClientApiGenerator(serviceGen, usedTopLevelNames, i),
);
}
}
}
/// Creates the fields in each message.
/// Resolves field types and extension targets using the supplied context.
void resolve(GenerationContext ctx) {
if (_linked) throw StateError('cross references already resolved');
for (final m in messageGenerators) {
m.resolve(ctx);
}
for (final x in extensionGenerators) {
x.resolve(ctx);
}
_linked = true;
}
@override
String get package => descriptor.package;
@override
String get classname => '';
@override
String get fullName => descriptor.package;
@override
FileGenerator get fileGen => this;
@override
ProtobufContainer? get parent => null;
@override
List<int> get fieldPath => [];
/// Generates all the Dart files for this .proto file.
List<CodeGeneratorResponse_File> generateFiles(OutputConfiguration config) {
if (!_linked) throw StateError('not linked');
CodeGeneratorResponse_File makeFile(String extension, String content) {
final protoUrl = Uri.file(descriptor.name);
final dartUrl = config.outputPathFor(protoUrl, extension);
return CodeGeneratorResponse_File()
..name = dartUrl.path
..content = content;
}
final mainWriter = generateMainFile(config);
final enumWriter = generateEnumFile(config);
final generateMetadata = options.generateMetadata;
final files = [
makeFile('.pb.dart', mainWriter.emitSource(format: !generateMetadata)),
makeFile(
'.pbenum.dart',
enumWriter.emitSource(format: !generateMetadata),
),
// TODO(devoncarew): Consider not emitting empty json files.
makeFile('.pbjson.dart', generateJsonFile(config)),
];
if (generateMetadata) {
files.addAll([
makeFile(
'.pb.dart.meta',
mainWriter.sourceLocationInfo.writeToJson().toString(),
),
makeFile(
'.pbenum.dart.meta',
enumWriter.sourceLocationInfo.writeToJson().toString(),
),
]);
}
if (options.useGrpc) {
if (grpcGenerators.isNotEmpty) {
files.add(makeFile('.pbgrpc.dart', generateGrpcFile(config)));
}
} else {
if (serviceGenerators.isNotEmpty) {
files.add(makeFile('.pbserver.dart', generateServerFile(config)));
}
}
return files;
}
/// Creates an IndentingWriter with metadata generation enabled or disabled.
IndentingWriter makeWriter() {
return IndentingWriter(
fileName: descriptor.name,
generateMetadata: options.generateMetadata,
);
}
/// Returns the contents of the .pb.dart file for this .proto file.
IndentingWriter generateMainFile([
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
if (!_linked) throw StateError('not linked');
final out = makeWriter();
writeMainHeader(out, config);
// Generate code.
for (final m in messageGenerators) {
m.generate(out);
}
// Generate code for extensions defined at top-level using a class name
// derived from the file name.
if (extensionGenerators.isNotEmpty) {
// TODO(antonm): do not generate a class.
final className = extensionClassName(descriptor, usedTopLevelNames);
out.addBlock('class $className {', '}\n', () {
for (final x in extensionGenerators) {
x.generate(out);
}
out.println(
'static void registerAllExtensions('
'$protobufImportPrefix.ExtensionRegistry registry) {',
);
for (final x in extensionGenerators) {
out.println(' registry.add(${x.name});');
}
out.println('}');
});
}
for (final c in clientApiGenerators) {
c.generate(out);
}
return out;
}
/// Writes the header and imports for the .pb.dart file.
void writeMainHeader(
IndentingWriter out, [
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
final importWriter = ImportWriter();
// We only add the dart:async import if there are generic client API
// generators for services in the FileDescriptorProto.
if (clientApiGenerators.isNotEmpty) {
importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
}
importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
if (_needsFixnumImport) {
importWriter.addImport(
'package:fixnum/fixnum.dart',
prefix: fixnumImportPrefix,
);
}
if (_needsProtobufImport) {
importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
}
for (final libraryUri in findMixinImports()) {
importWriter.addImport(libraryUri, prefix: mixinImportPrefix);
}
// Import the .pb.dart files we depend on.
final imports = Set<FileGenerator>.identity();
final enumImports = Set<FileGenerator>.identity();
_findProtosToImport(imports, enumImports);
for (final target in imports) {
_addImport(importWriter, config, target, '.pb.dart');
}
for (final target in enumImports) {
// If we're already adding the main file (.pb.dart) as an import, we don't
// need to add the enums file, as that's exported from the main file.
if (!imports.contains(target)) {
_addImport(importWriter, config, target, '.pbenum.dart');
}
}
importWriter.addExport(
_protobufImportUrl,
members: ['GeneratedMessageGenericExtensions'],
);
for (final publicDependency in descriptor.publicDependency) {
_addExport(
importWriter,
config,
Uri.file(descriptor.dependency[publicDependency]),
'.pb.dart',
);
}
// Export enums in main file for backward compatibility.
if (hasEnums) {
final url = config.resolveImport(
protoFileUri,
protoFileUri,
'.pbenum.dart',
);
importWriter.addExport(url.toString());
}
// The well-known-types mixins create src/ refs into package:protobuf; we
// should likely refactor this so they're regular (non-src/) references.
//
// For now, we surpress the analysis warning.
_writeHeading(
out,
extraIgnores: {if (importWriter.hasSrcImport) 'implementation_imports'},
);
out.println(importWriter.emit());
}
bool get _needsFixnumImport {
for (final m in messageGenerators) {
if (m.needsFixnumImport) return true;
}
for (final x in extensionGenerators) {
if (x.needsFixnumImport) return true;
}
return false;
}
bool get _needsProtobufImport =>
messageGenerators.isNotEmpty ||
extensionGenerators.isNotEmpty ||
clientApiGenerators.isNotEmpty;
/// Returns the generator for each .pb.dart file we need to import.
void _findProtosToImport(
Set<FileGenerator> imports,
Set<FileGenerator> enumImports,
) {
for (final m in messageGenerators) {
m.addImportsTo(imports, enumImports);
}
for (final x in extensionGenerators) {
x.addImportsTo(imports, enumImports);
}
// Add imports needed for client-side services.
for (final x in serviceGenerators) {
x.addImportsTo(imports);
}
// Don't need to import self. (But we may need to import the enums.)
imports.remove(this);
}
/// Returns a sorted list of imports needed to support all mixins.
List<String> findMixinImports() {
final mixins = <PbMixin>{};
for (final m in messageGenerators) {
m.addMixinsTo(mixins);
}
return mixins
.map((mixin) => mixin.importFrom)
.toSet()
.toList(growable: false)..sort();
}
/// Returns the contents of the .pbenum.dart file for this .proto file.
IndentingWriter generateEnumFile([
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
if (!_linked) throw StateError('not linked');
final out = makeWriter();
_writeHeading(out);
final importWriter = ImportWriter();
if (hasEnums) {
// Make sure any other symbols in dart:core don't cause name conflicts
// with enums that have the same name.
importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
}
for (final publicDependency in descriptor.publicDependency) {
_addExport(
importWriter,
config,
Uri.file(descriptor.dependency[publicDependency]),
'.pbenum.dart',
);
}
if (importWriter.hasImports) {
out.println(importWriter.emit());
}
for (final e in enumGenerators) {
e.generate(out);
}
for (final m in messageGenerators) {
m.generateEnums(out);
}
return out;
}
/// Returns the number of enum types generated in the .pbenum.dart file.
int get enumCount {
var count = enumGenerators.length;
for (final m in messageGenerators) {
count += m.enumCount;
}
return count;
}
/// Returns whether this proto file defines any enums (either top level or
/// nested within messages).
bool get hasEnums => enumCount > 0;
/// Returns the contents of the .pbserver.dart file for this .proto file.
String generateServerFile([
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
if (!_linked) throw StateError('not linked');
final out = makeWriter();
_writeHeading(
out,
extraIgnores: {'deprecated_member_use_from_same_package'},
);
final importWriter = ImportWriter();
if (serviceGenerators.isNotEmpty) {
importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
}
// Import .pb.dart files needed for requests and responses.
final imports = <FileGenerator>{};
for (final x in serviceGenerators) {
x.addImportsTo(imports);
}
for (final target in imports) {
_addImport(importWriter, config, target, '.pb.dart');
}
// Import .pbjson.dart file needed for $json and $messageJson.
if (serviceGenerators.isNotEmpty) {
_addImport(importWriter, config, this, '.pbjson.dart');
}
final url = config.resolveImport(protoFileUri, protoFileUri, '.pb.dart');
importWriter.addExport(url.toString());
if (importWriter.hasImports) {
out.println(importWriter.emit());
}
for (final s in serviceGenerators) {
s.generate(out);
}
return out.emitSource(format: true);
}
/// Returns the contents of the .pbgrpc.dart file for this .proto file.
String generateGrpcFile([
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
if (!_linked) throw StateError('not linked');
final out = makeWriter();
_writeHeading(out);
final importWriter = ImportWriter();
importWriter.addImport(_asyncImportUrl, prefix: asyncImportPrefix);
importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
importWriter.addImport(_grpcImportUrl, prefix: grpcImportPrefix);
importWriter.addImport(_protobufImportUrl, prefix: protobufImportPrefix);
// Import .pb.dart files needed for requests and responses.
final imports = <FileGenerator>{};
for (final generator in grpcGenerators) {
generator.addImportsTo(imports);
}
for (final target in imports) {
_addImport(importWriter, config, target, '.pb.dart');
}
final url = config.resolveImport(protoFileUri, protoFileUri, '.pb.dart');
importWriter.addExport(url.toString());
out.println(importWriter.emit());
for (final generator in grpcGenerators) {
generator.generate(out);
}
return out.emitSource(format: true);
}
void writeBinaryDescriptor(
IndentingWriter out,
String identifierName,
String name,
GeneratedMessage descriptor,
) {
final base64 = base64Encode(descriptor.writeToBuffer());
out.println(
'/// Descriptor for `$name`. Decode as a '
'`${descriptor.info_.qualifiedMessageName}`.',
);
const indent = ' ';
final base64Lines = _splitString(
base64,
74,
).map((s) => "'$s'").join('\n$indent');
out.println(
'final $_typedDataImportPrefix.Uint8List '
'$identifierName = '
'$_convertImportPrefix.base64Decode(\n$indent$base64Lines);',
);
}
/// Return the given [str], split into separate segments, where no segment is
/// longer than [segmentLength].
static List<String> _splitString(String str, int segmentLength) {
final result = <String>[];
while (str.length >= segmentLength) {
result.add(str.substring(0, segmentLength));
str = str.substring(segmentLength);
}
if (str.isNotEmpty) result.add(str);
return result;
}
/// Returns the contents of the .pbjson.dart file for this .proto file.
String generateJsonFile([
OutputConfiguration config = const DefaultOutputConfiguration(),
]) {
if (!_linked) throw StateError('not linked');
final out = makeWriter();
_writeHeading(out, extraIgnores: {'unused_import'});
final importWriter = ImportWriter();
importWriter.addImport(_convertImportUrl, prefix: _convertImportPrefix);
importWriter.addImport(_coreImportUrl, prefix: coreImportPrefix);
importWriter.addImport(_typedDataImportUrl, prefix: _typedDataImportPrefix);
// Import the .pbjson.dart files we depend on.
final imports = _findJsonProtosToImport();
for (final target in imports) {
_addImport(importWriter, config, target, '.pbjson.dart');
}
out.println(importWriter.emit());
for (final e in enumGenerators) {
e.generateConstants(out);
writeBinaryDescriptor(
out,
e.binaryDescriptorName,
e._descriptor.name,
e._descriptor,
);
out.println('');
}
for (final m in messageGenerators) {
m.generateConstants(out);
writeBinaryDescriptor(
out,
m.binaryDescriptorName,
m._descriptor.name,
m._descriptor,
);
out.println('');
}
for (final s in serviceGenerators) {
s.generateConstants(out);
writeBinaryDescriptor(
out,
s.binaryDescriptorName,
s._descriptor.name,
s._descriptor,
);
out.println('');
}
return out.emitSource(format: true);
}
/// Returns the generator for each .pbjson.dart file the generated
/// .pbjson.dart needs to import.
Set<FileGenerator> _findJsonProtosToImport() {
final imports = Set<FileGenerator>.identity();
for (final m in messageGenerators) {
m.addConstantImportsTo(imports);
}
for (final x in extensionGenerators) {
x.addConstantImportsTo(imports);
}
for (final x in serviceGenerators) {
x.addConstantImportsTo(imports);
}
imports.remove(this); // Don't need to import self.
return imports;
}
/// Writes the header at the top of the dart file.
void _writeHeading(
IndentingWriter out, {
Set<String> extraIgnores = const <String>{},
}) {
final ignores = ({..._fileIgnores, ...extraIgnores}).toList()..sort();
// Group the ignores into lines not longer than 80 chars.
final ignorelines = <String>[];
if (ignores.isNotEmpty) {
ignorelines.add('// ignore_for_file: ${ignores.first}');
for (final ignore in ignores.skip(1)) {
if (ignorelines.last.length + ignore.length + ', '.length > 80) {
ignorelines.add('// ignore_for_file: $ignore');
} else {
ignorelines.add('${ignorelines.removeLast()}, $ignore');
}
}
}
out.println('''
// This is a generated file - do not edit.
//
// Generated from ${descriptor.name}.
// @dart = 3.3
''');
ignorelines.forEach(out.println);
out.println('');
}
/// Writes an import of a .dart file corresponding to a .proto file.
/// (Possibly the same .proto file.)
void _addImport(
ImportWriter importWriter,
OutputConfiguration config,
FileGenerator target,
String ext,
) {
final url = config.resolveImport(target.protoFileUri, protoFileUri, ext);
final import = url.toString();
// .pb.dart files should always be prefixed -- the protoFileUri check will
// evaluate to true not just for the main .pb.dart file based off the proto
// file, but also for the .pbserver.dart, .pbgrpc.dart files.
if (ext == '.pb.dart' || protoFileUri != target.protoFileUri) {
importWriter.addImport(import, prefix: fileGen.importPrefix(target));
} else {
importWriter.addImport(import);
}
}
/// Writes an export of a pb.dart file corresponding to a .proto file.
/// (Possibly the same .proto file.)
void _addExport(
ImportWriter importWriter,
OutputConfiguration config,
Uri target,
String ext,
) {
final url = config.resolveImport(target, protoFileUri, ext);
importWriter.addExport(url.toString());
}
}
class ConditionalConstDefinition {
final String envName;
final String constFieldName;
ConditionalConstDefinition(this.envName)
: constFieldName = _convertToCamelCase(envName);
String get constDefinition {
return 'const $coreImportPrefix.bool $constFieldName = '
"$coreImportPrefix.bool.fromEnvironment(${quoted('protobuf.$envName')});";
}
String createTernary(String ifFalse) {
return "$constFieldName ? '' : ${quoted(ifFalse)}";
}
// Convert foo_bar_baz to _fooBarBaz.
static String _convertToCamelCase(String lowerUnderscoreCase) {
final parts = lowerUnderscoreCase.split('_');
final rest =
parts.skip(1).map((item) {
return item.substring(0, 1).toUpperCase() + item.substring(1);
}).join();
return '_${parts.first}$rest';
}
}
Edition _getEdition(FileDescriptorProto file) {
if (file.edition != Edition.EDITION_UNKNOWN) {
return file.edition;
}
if (file.syntax == 'proto3') {
return Edition.EDITION_PROTO3;
}
return Edition.EDITION_PROTO2;
}
FeatureSet resolveFeatures(FeatureSet parent, FeatureSet child) {
final result = parent.deepCopy();
result.mergeFromMessage(child);
return result;
}
FeatureSet _getEditionDefaults(
FeatureSetDefaults editionDefaults,
Edition edition,
) {
if (edition.value < editionDefaults.minimumEdition.value) {
throw ArgumentError(
'Edition $edition is earlier than the minimum supported edition ${editionDefaults.minimumEdition}!',
);
}
if (edition.value > editionDefaults.maximumEdition.value) {
throw ArgumentError(
'Edition $edition is later than the maximum supported edition ${editionDefaults.maximumEdition}!',
);
}
FeatureSetDefaults_FeatureSetEditionDefault? found;
for (final d in editionDefaults.defaults) {
if (d.edition.value > edition.value) {
break;
}
found = d;
}
if (found == null) {
throw ArgumentError('No default found for edition $edition!');
}
final defaults = found.fixedFeatures.deepCopy();
defaults.mergeFromMessage(found.overridableFeatures);
return defaults;
}
/// Reads and the declared mixins in the file, keyed by name.
///
/// Performs some basic validation on declared mixins, e.g. whether names
/// are valid dart identifiers and whether there are cycles in the `parent`
/// hierarchy.
/// Does not check for existence of import files or classes.
Map<String, PbMixin> _getDeclaredMixins(FileDescriptorProto desc) {
String mixinError(String error) => 'Option "mixins" in ${desc.name}: $error';
if (!desc.hasOptions() || !desc.options.hasExtension(Dart_options.imports)) {
return <String, PbMixin>{};
}
final dartMixins = <String, DartMixin>{};
final importedMixins =
desc.options.getExtension(Dart_options.imports) as Imports;
for (final mixin in importedMixins.mixins) {
if (dartMixins.containsKey(mixin.name)) {
throw mixinError('Duplicate mixin name: "${mixin.name}"');
}
if (!mixin.name.startsWith(_dartIdentifier)) {
throw mixinError('"${mixin.name}" is not a valid dart class identifier');
}
if (mixin.hasParent() && !mixin.parent.startsWith(_dartIdentifier)) {
throw mixinError(
'Mixin parent "${mixin.parent}" of "${mixin.name}" is '
'not a valid dart class identifier',
);
}
dartMixins[mixin.name] = mixin;
}
// Detect cycles and unknown parents.
for (final mixin in dartMixins.values) {
if (!mixin.hasParent()) continue;
var currentMixin = mixin;
final parentChain = <String>[];
while (currentMixin.hasParent()) {
final parentName = currentMixin.parent;
final declaredMixin = dartMixins.containsKey(parentName);
final internalMixin = !declaredMixin && findMixin(parentName) != null;
if (internalMixin) break; // No further validation of parent chain.
if (!declaredMixin) {
throw mixinError(
'Unknown mixin parent "${mixin.parent}" of '
'"${currentMixin.name}"',
);
}
if (parentChain.contains(parentName)) {
final cycle = '${parentChain.join('->')}->$parentName';
throw mixinError('Cycle in parent chain: $cycle');
}
parentChain.add(parentName);
currentMixin = dartMixins[parentName]!;
}
}
// Turn DartMixins into PbMixins.
final pbMixins = <String, PbMixin>{};
PbMixin? resolveMixin(String name) {
if (pbMixins.containsKey(name)) return pbMixins[name];
if (dartMixins.containsKey(name)) {
final dartMixin = dartMixins[name]!;
final pbMixin = PbMixin(
dartMixin.name,
importFrom: dartMixin.importFrom,
parent: resolveMixin(dartMixin.parent),
);
pbMixins[name] = pbMixin;
return pbMixin;
}
return findMixin(name);
}
for (final mixin in dartMixins.values) {
resolveMixin(mixin.name);
}
return pbMixins;
}
const _fileIgnores = {
'annotate_overrides',
'camel_case_types',
'comment_references',
'constant_identifier_names',
'curly_braces_in_flow_control_structures',
'deprecated_member_use_from_same_package',
'library_prefixes',
'non_constant_identifier_names',
};