blob: 8789c355412826c697f35af7912bd2128becb3fe [file] [edit]
// Copyright (c) 2026, 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.
import 'dart:io';
import 'package:path/path.dart' as p;
import 'package:yaml/yaml.dart';
import 'code_generator/imports.dart' show builtInLibraries;
/// Migrates a YAML configuration file to a Dart configuration script.
///
/// Reads [yamlConfig] and writes a Dart configuration script to [outputDart].
void migrate({required File yamlConfig, required File outputDart}) {
if (!yamlConfig.existsSync()) {
throw FileSystemException(
'YAML config file does not exist',
yamlConfig.path,
);
}
final yamlContent = yamlConfig.readAsStringSync();
final yamlMap = loadYaml(yamlContent) as YamlMap;
final configDirectory = yamlConfig.parent.absolute;
var rootDir = configDirectory;
while (!File(p.join(rootDir.path, 'pubspec.yaml')).existsSync()) {
final parent = rootDir.parent;
if (parent.path == rootDir.path) break;
rootDir = parent;
}
final packageRoot = rootDir;
final isAtRoot =
p.normalize(configDirectory.path) == p.normalize(packageRoot.path);
final configDir = isAtRoot ? null : configDirectory;
final outputDir = yamlConfig.path.contains(p.join('test', 'migrate', 'yaml'))
? p.join(packageRoot.path, 'test', 'migrate', 'dart')
: outputDart.parent.path;
final relToPkg = p.posix.normalize(
p.relative(packageRoot.path, from: outputDir).replaceAll(r'\', '/'),
);
final packageRootExpr = (relToPkg == '.' || relToPkg.isEmpty)
? "Platform.script.resolve('./')"
: "Platform.script.resolve('$relToPkg/')";
String resolvePath(String rawPath) {
if (rawPath.startsWith(r'$XCODE/')) {
return "xcodeUri.resolve('${p.posix.normalize(rawPath.substring(7))}')";
}
if (rawPath == r'$XCODE') return 'xcodeUri';
if (rawPath.startsWith(r'$IOS_SDK/')) {
return "iosSdkUri.resolve('${p.posix.normalize(rawPath.substring(9))}')";
}
if (rawPath == r'$IOS_SDK') return 'iosSdkUri';
if (rawPath.startsWith(r'$MACOS_SDK/')) {
return "macSdkUri.resolve('${p.posix.normalize(rawPath.substring(11))}')";
}
if (rawPath == r'$MACOS_SDK') return 'macSdkUri';
if (rawPath.startsWith('package:')) {
final uri = Uri.parse(rawPath);
final rest = uri.pathSegments.skip(1).join('/');
if (File(p.join(packageRoot.path, 'lib', rest)).existsSync()) {
return "packageRoot.resolve('lib/$rest')";
}
final pkgName = uri.pathSegments.first;
if (File(
p.join(packageRoot.path, 'example', pkgName, 'lib', rest),
).existsSync()) {
return "packageRoot.resolve('example/$pkgName/lib/$rest')";
}
return "Uri.parse('$rawPath')";
}
if (rawPath.startsWith('http:') || rawPath.startsWith('https:')) {
return "Uri.parse('$rawPath')";
}
if (p.isAbsolute(rawPath)) {
return "Uri.file('$rawPath')";
}
final posixPath = p.posix.normalize(rawPath.replaceAll(r'\', '/'));
return configDir == null
? "packageRoot.resolve('$posixPath')"
: "configDir.resolve('$posixPath')";
}
String resolveCompilerOption(String opt) {
if (!opt.startsWith('-I')) return _str(opt);
final incPath = opt.substring(2);
if (!p.isRelative(incPath)) return _str(opt);
final posixInc = p.posix.normalize(incPath.replaceAll(r'\', '/'));
final fromPkg = p.normalize(p.join(packageRoot.path, incPath));
final fromCfg = configDir != null
? p.normalize(p.join(configDir.path, incPath))
: fromPkg;
final expr =
configDir == null ||
(FileSystemEntity.typeSync(fromPkg) !=
FileSystemEntityType.notFound &&
FileSystemEntity.typeSync(fromCfg) ==
FileSystemEntityType.notFound)
? "packageRoot.resolve('$posixInc').toFilePath()"
: "configDir.resolve('$posixInc').toFilePath()";
final optStr = "'-I\${$expr}'";
return optStr.length + 10 > 80
? '// ignore: lines_longer_than_80_chars\n$optStr'
: optStr;
}
final buf = StringBuffer();
// Static imports block
buf.writeln('// ignore_for_file: unused_import');
buf.writeln("import 'dart:io';");
buf.writeln("import 'package:ffigen/ffigen.dart';");
buf.writeln("import 'package:glob/glob.dart';\n");
_emitTopLevelDeclarations(buf, yamlMap);
buf.writeln('Future<void> main() async {');
buf.writeln('final packageRoot = $packageRootExpr;');
if (configDir != null) {
final relConfig = p.posix.normalize(
p.relative(configDir.path, from: packageRoot.path).replaceAll(r'\', '/'),
);
buf.writeln("final configDir = packageRoot.resolve('$relConfig/');");
}
buf.writeln('await FfiGenerator(');
_emitOutput(buf, yamlMap, resolvePath);
_emitInput(buf, yamlMap, resolvePath, resolveCompilerOption);
_emitObjectiveC(buf, yamlMap);
if (yamlMap.containsKey('cpp')) {
buf.writeln('cpp: const Cpp(),');
}
_emitImportTypeArgument(buf, yamlMap, resolvePath);
_emitVisitors(buf, yamlMap);
buf.writeln(').generate();');
buf.writeln('}\n');
final parentDir = outputDart.parent;
if (!parentDir.existsSync()) {
parentDir.createSync(recursive: true);
}
outputDart.writeAsStringSync(buf.toString());
Process.runSync(Platform.resolvedExecutable, [
'format',
outputDart.absolute.path,
], workingDirectory: outputDart.parent.absolute.path);
}
String _str(String s) {
if (s.contains('\n')) {
final escaped = s
.replaceAll(r'\', r'\\')
.replaceAll("'''", r"\'\'\'")
.replaceAll(r'$', r'\$');
return "'''\n$escaped'''";
}
final escaped = s
.replaceAll(r'\', r'\\')
.replaceAll("'", r"\'")
.replaceAll(r'$', r'\$');
return "'$escaped'";
}
String _strSet(Iterable<String> s) => '{${s.map(_str).join(', ')}}';
bool _isExact(String s) => RegExp(r'^[a-zA-Z_0-9:]+$').hasMatch(s);
String _regexPat(String s) {
final pat = s.startsWith('^') ? s.substring(1) : s;
return pat.endsWith(r'$') ? pat.substring(0, pat.length - 1) : pat;
}
String _nameCond(String target, String name) {
if (_isExact(name)) return '$target == ${_str(name)}';
final pat = _regexPat(name);
return 'RegExp(r\'^$pat\$\').hasMatch($target)';
}
void _emitTopLevelDeclarations(StringBuffer buf, YamlMap yamlMap) {
final typeMap = yamlMap['type-map'];
if (typeMap is! Map || typeMap.isEmpty) return;
final libImports = yamlMap['library-imports'] as Map? ?? {};
String libImportPath(String lib) {
if (builtInLibraries.containsKey(lib)) {
return builtInLibraries[lib]!.importPath(false);
}
return libImports[lib]?.toString() ?? lib;
}
buf.writeln('final _importedTypes = <String, ImportedType>{');
for (final section in typeMap.values) {
if (section is Map) {
for (final entry in section.entries) {
final typeName = entry.key as String;
final typeInfo = entry.value as Map;
final lib = typeInfo['lib'] as String? ?? 'ffi';
final cType = typeInfo['c-type'] as String? ?? typeName;
final dartType = typeInfo['dart-type'] as String? ?? typeName;
final importPath = libImportPath(lib);
buf.writeln(
"'$typeName': ImportedType("
"const LibraryImport('$lib', '$importPath'), "
"'$cType', "
"'$dartType', "
"'$typeName',"
'),',
);
}
}
}
buf.writeln('};\n');
buf.writeln(
'ImportedType? importType(Declaration declaration) => '
'_importedTypes[declaration.originalName];\n',
);
}
void _emitOutput(
StringBuffer buf,
YamlMap yamlMap,
String Function(String) resolvePath,
) {
final hasObjc = yamlMap['language'] == 'objc';
final hasCpp = yamlMap.containsKey('cpp');
final output = yamlMap['output'];
String rawDart;
String? rawObjc;
String? rawSymbolOutput;
String? rawSymbolImport;
if (output is String) {
rawDart = output;
rawObjc = hasObjc ? '$output.m' : null;
} else if (output is Map) {
rawDart = output['bindings'] as String;
if (output.containsKey('objc-bindings')) {
rawObjc = output['objc-bindings'] as String;
} else if (hasObjc) {
rawObjc = '$rawDart.m';
}
if (output.containsKey('symbol-file')) {
final sym = output['symbol-file'] as Map;
rawSymbolOutput = sym['output'] as String;
rawSymbolImport = sym['import-path'] as String;
}
} else {
throw ArgumentError('Invalid output: $output');
}
buf.writeln('output: Output(');
buf.writeln('dart: DartOutput(path: ${resolvePath(rawDart)}),');
if (rawObjc != null) {
buf.writeln('objectiveCFile: ${resolvePath(rawObjc)},');
}
if (hasCpp) {
buf.writeln('cppFile: ${resolvePath('$rawDart.cpp')},');
}
if (rawSymbolOutput != null) {
buf.writeln(
'symbolFile: SymbolFile('
"Uri.parse('$rawSymbolImport'), "
'${resolvePath(rawSymbolOutput)}'
'),',
);
}
if (yamlMap.containsKey('ffi-native')) {
final ffiNative = yamlMap['ffi-native'];
final assetId = ffiNative is Map
? (ffiNative['asset-id'] ?? ffiNative['assetId'])
: null;
if (assetId != null) {
buf.writeln("style: const NativeExternalBindings(assetId: '$assetId'),");
} else {
buf.writeln('style: const NativeExternalBindings(),');
}
} else {
final name = yamlMap['name'] as String?;
final description = yamlMap['description'] as String?;
if (name != null || description != null) {
buf.writeln('style: const DynamicLibraryBindings(');
if (name != null) buf.writeln("wrapperName: '$name',");
if (description != null) {
buf.writeln('wrapperDocComment: ${_str(description)},');
}
buf.writeln('),');
}
}
final comments = yamlMap['comments'];
if (comments == false) {
buf.writeln('commentType: const CommentType.none(),');
} else if (comments is Map) {
final length = comments['length'];
final style = comments['style'];
if (length == 'none') {
buf.writeln('commentType: const CommentType.none(),');
} else if (length == 'brief') {
buf.writeln(
'commentType: const CommentType('
"CommentStyle.${style ?? 'doxygen'}, CommentLength.brief),",
);
} else if (style == 'any') {
buf.writeln(
'commentType: const CommentType(CommentStyle.any, CommentLength.full),',
);
}
}
final preamble = yamlMap['preamble'] as String?;
if (preamble != null && preamble.isNotEmpty) {
buf.writeln('preamble: ${_str(preamble)},');
}
if (yamlMap['format'] == false) {
buf.writeln('format: false,');
}
buf.writeln('),');
}
String _globPattern(String p) {
if (p.startsWith('/**')) return p;
if (p.startsWith('**')) return '/$p';
if (p.startsWith('/')) return p;
return '/**/$p';
}
void _emitHeaders(
StringBuffer buf,
Map<dynamic, dynamic>? headers,
String Function(String) resolvePath,
) {
if (headers == null) return;
final entryPoints = headers['entry-points'] as List?;
if (entryPoints != null && entryPoints.isNotEmpty) {
buf.writeln('entryPoints: [');
for (final ep in entryPoints) {
buf.writeln('${resolvePath(ep.toString())},');
}
buf.writeln('],');
}
final includeDirectives = headers['include-directives'] as List?;
if (includeDirectives != null && includeDirectives.isNotEmpty) {
final globChecks = includeDirectives
.map((p) => "Glob('${_globPattern(p.toString())}').matches(uri.path)")
.join(' || ');
buf.writeln('include: (uri) => $globChecks,');
}
}
void _emitCompilerOptions(
StringBuffer buf,
YamlMap yamlMap,
String Function(String) resolveCompilerOption,
) {
final compilerOpts = yamlMap['compiler-opts'];
if (compilerOpts == null) return;
final List<String> opts;
if (compilerOpts is String) {
opts = compilerOpts
.split(' ')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
} else if (compilerOpts is List) {
opts = compilerOpts.map((e) => e.toString()).toList();
} else {
opts = const [];
}
if (opts.isEmpty) return;
final auto = yamlMap['compiler-opts-automatic'] as Map?;
final macAuto = auto?['macos'] as Map?;
final includeMacStdLib = macAuto?['include-c-standard-library'] != false;
final hasSysroot = opts.any(
(o) => o.contains('-isysroot') || o.contains('MacOSX.sdk'),
);
buf.writeln('compilerOptions: [');
for (final opt in opts) {
buf.writeln('${resolveCompilerOption(opt)},');
}
if (includeMacStdLib && !hasSysroot) {
buf.writeln("if (Platform.isMacOS) ...['-isysroot', macSdkPath],");
}
buf.writeln('],');
}
void _emitInput(
StringBuffer buf,
YamlMap yamlMap,
String Function(String) resolvePath,
String Function(String) resolveCompilerOption,
) {
buf.writeln('input: Input(');
_emitHeaders(buf, yamlMap['headers'] as Map?, resolvePath);
_emitCompilerOptions(buf, yamlMap, resolveCompilerOption);
if (yamlMap['ignore-source-errors'] == true) {
buf.writeln('ignoreSourceErrors: true,');
}
buf.writeln('),');
}
void _emitObjectiveC(StringBuffer buf, YamlMap yamlMap) {
final genPkgObjC = yamlMap['generate-for-package-objective-c'] == true;
if (yamlMap['language'] != 'objc' && !genPkgObjC) return;
final extVersions = yamlMap['external-versions'] as Map?;
if (extVersions != null) {
buf.writeln('objectiveC: ObjectiveC(');
buf.writeln('externalVersions: ExternalVersions(');
if (extVersions.containsKey('ios')) {
final iosMin = (extVersions['ios'] as Map)['min'].toString();
buf.writeln('ios: Versions(min: ${_formatVersion(iosMin)}),');
}
if (extVersions.containsKey('macos')) {
final macosMin = (extVersions['macos'] as Map)['min'].toString();
buf.writeln('macos: Versions(min: ${_formatVersion(macosMin)}),');
}
buf.writeln('),');
if (genPkgObjC) {
buf.writeln('generateForPackageObjectiveC: true,');
}
buf.writeln('),');
} else if (genPkgObjC) {
buf.writeln(
'objectiveC: const ObjectiveC(generateForPackageObjectiveC: true),',
);
} else {
buf.writeln('objectiveC: const ObjectiveC(),');
}
}
String _formatVersion(String v) {
final parts = v.split('.').map((s) => int.tryParse(s) ?? 0).toList();
while (parts.length < 3) {
parts.add(0);
}
return 'Version(${parts[0]}, ${parts[1]}, ${parts[2]})';
}
void _emitImportTypeArgument(
StringBuffer buf,
YamlMap yamlMap,
String Function(String) resolvePath,
) {
final typeMap = yamlMap['type-map'];
final hasTypeMap = typeMap is Map && typeMap.isNotEmpty;
final importMap = yamlMap['import'];
final syms = importMap is Map
? (importMap['symbol-files'] as List?)?.cast<String>()
: null;
if (syms != null && syms.isNotEmpty && !hasTypeMap) {
if (syms.length == 1) {
buf.writeln(
'importType: importFromSymbolFile(${resolvePath(syms.first)}),',
);
} else {
final exprs = syms.map(resolvePath).join(', ');
buf.writeln('importType: importFromSymbolFiles([$exprs]),');
}
} else if (hasTypeMap) {
buf.writeln('importType: importType,');
}
}
void _emitDeclarationFilter(
StringBuffer buf,
Map<dynamic, dynamic>? cfg, {
String target = 'node.originalName',
bool defaultInclude = false,
}) {
if (cfg == null) {
if (defaultInclude) buf.writeln('node.isIncluded = true;');
return;
}
final includes = (cfg['include'] as List?)?.cast<String>();
final excludes = (cfg['exclude'] as List?)?.cast<String>();
if (includes == null && excludes == null) {
if (defaultInclude) buf.writeln('node.isIncluded = true;');
return;
}
final cond = _buildCondition(includes, excludes, target);
buf.writeln('node.isIncluded = $cond;');
}
void _emitRenames(
StringBuffer buf,
Map<dynamic, dynamic>? cfg, {
String target = 'node.name',
}) {
final rename = cfg?['rename'] as Map?;
if (rename == null || rename.isEmpty) return;
for (final entry in rename.entries) {
final from = entry.key.toString();
final to = entry.value.toString();
if (_isExact(from)) {
buf.writeln("if ($target == '$from') { $target = '$to'; }");
} else if (!to.contains(r'$')) {
final pat = _regexPat(from);
buf.writeln(
"if (RegExp(r'^$pat\$').hasMatch($target)) { $target = '$to'; }",
);
} else {
final pat = _regexPat(from);
buf.writeln('''
if (RegExp(r'^$pat\$').firstMatch($target) case final match?) {
node.name = r'$to'.replaceAllMapped(RegExp(r'\\\$([0-9])'), (m) => match[int.parse(m[1]!)] ?? '');
}''');
}
}
}
void _emitFuncVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
if (!yamlMap.containsKey('functions') && excludeAll) return;
final cfg = yamlMap['functions'] as Map?;
buf.writeln('func: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
if (cfg != null) {
final leaf = cfg['leaf'];
if (leaf is bool) {
buf.writeln('node.isLeaf = $leaf;');
} else if (leaf is Map) {
final cond = _buildCondition(
(leaf['include'] as List?)?.cast<String>(),
(leaf['exclude'] as List?)?.cast<String>(),
'node.originalName',
);
if (cond == 'true') {
buf.writeln('node.isLeaf = true;');
} else if (cond != 'false') {
buf.writeln('node.isLeaf = $cond;');
}
}
final symAddr = cfg['symbol-address'] as Map?;
final symInc = (symAddr?['include'] as List?)?.cast<String>();
if (symInc != null && symInc.isNotEmpty) {
final cond = _buildCondition(symInc, null, 'node.originalName');
buf.writeln('if ($cond) { node.exposeSymbolAddress = true; }');
}
final expTypedefs = cfg['expose-typedefs'] as Map?;
final expInc = (expTypedefs?['include'] as List?)?.cast<String>();
if (expInc != null && expInc.isNotEmpty) {
final cond = _buildCondition(expInc, null, 'node.originalName');
buf.writeln('if ($cond) { node.generateTypedefs = true; }');
}
}
buf.writeln('},');
}
void _emitStructVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
if (!yamlMap.containsKey('structs') && excludeAll) return;
final cfg = yamlMap['structs'] as Map?;
buf.writeln('struct: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
final depOnly = cfg?['dependency-only'] == 'opaque' ? 'opaque' : 'full';
buf.writeln('node.dependencies = CompoundDependencies.$depOnly;');
if (cfg?['pack'] is Map) {
for (final entry in (cfg!['pack'] as Map).entries) {
final cond = _nameCond('node.originalName', entry.key.toString());
final val = entry.value == 'none' ? 'null' : entry.value.toString();
buf.writeln('if ($cond) { node.pack = $val; }');
}
}
buf.writeln('},');
}
void _emitUnionVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
if (!yamlMap.containsKey('unions') && excludeAll) return;
final cfg = yamlMap['unions'] as Map?;
buf.writeln('union: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
final depOnly = cfg?['dependency-only'] == 'opaque' ? 'opaque' : 'full';
buf.writeln('node.dependencies = CompoundDependencies.$depOnly;');
buf.writeln('},');
}
void _emitEnumVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
final silenceWarn = yamlMap['silence-enum-warning'] == true;
if (!yamlMap.containsKey('enums') && !silenceWarn && excludeAll) return;
final cfg = yamlMap['enums'] as Map?;
buf.writeln('enumClass: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
final asInt = cfg?['as-int'] as Map?;
final asIntInc = (asInt?['include'] as List?)?.cast<String>();
if (asIntInc != null && asIntInc.isNotEmpty) {
final cond = _buildCondition(asIntInc, null, 'node.originalName');
buf.writeln('if ($cond) { node.style = EnumStyle.intConstants; }');
}
if (silenceWarn) {
buf.writeln('node.silenceWarning = true;');
}
buf.writeln('},');
}
void _emitUnnamedEnumVisitor(StringBuffer buf, YamlMap yamlMap) {
if (!yamlMap.containsKey('unnamed-enums')) return;
final cfg = yamlMap['unnamed-enums'] as Map?;
buf.writeln('unnamedEnumConstant: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: false);
_emitRenames(buf, cfg);
buf.writeln('},');
}
void _emitGlobalVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
if (!yamlMap.containsKey('globals') && excludeAll) return;
final cfg = yamlMap['globals'] as Map?;
buf.writeln('global: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
final symAddr = cfg?['symbol-address'] as Map?;
final symInc = (symAddr?['include'] as List?)?.cast<String>();
if (symInc != null && symInc.isNotEmpty) {
final cond = _buildCondition(symInc, null, 'node.originalName');
buf.writeln('if ($cond) { node.exposeSymbolAddress = true; }');
}
buf.writeln('},');
}
void _emitMacroVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
if (!yamlMap.containsKey('macros') && excludeAll) return;
final cfg = yamlMap['macros'] as Map?;
buf.writeln('macroConstant: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: !excludeAll);
_emitRenames(buf, cfg);
buf.writeln('},');
}
void _emitTypealiasVisitor(StringBuffer buf, YamlMap yamlMap) {
final excludeAll = yamlMap['exclude-all-by-default'] == true;
final includeUnused = yamlMap['include-unused-typedefs'] == true;
if (!yamlMap.containsKey('typedefs') && !includeUnused && excludeAll) return;
final cfg = yamlMap['typedefs'] as Map?;
buf.writeln('typealias: (node) {');
final incList = (cfg?['include'] as List?)?.cast<String>();
final excList = (cfg?['exclude'] as List?)?.cast<String>();
if ((incList != null && incList.isNotEmpty) ||
(excList != null && excList.isNotEmpty)) {
final cond = _buildCondition(incList, excList, 'node.originalName');
final trueVal = includeUnused
? 'TypealiasInclude.always'
: 'TypealiasInclude.ifUsed';
buf.writeln(
'node.isIncluded = ($cond) ? $trueVal : TypealiasInclude.never;',
);
} else if (includeUnused) {
buf.writeln('node.isIncluded = TypealiasInclude.always;');
} else if (!excludeAll) {
buf.writeln('node.isIncluded = TypealiasInclude.ifUsed;');
}
_emitRenames(buf, cfg);
buf.writeln('},');
}
void _emitObjCInterfaceVisitor(StringBuffer buf, YamlMap yamlMap) {
if (!yamlMap.containsKey('objc-interfaces') &&
yamlMap['include-transitive-objc-categories'] != false) {
return;
}
final cfg = yamlMap['objc-interfaces'] as Map?;
buf.writeln('objCInterface: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: false);
_emitRenames(buf, cfg);
if (cfg?['module'] is Map) {
for (final entry in (cfg!['module'] as Map).entries) {
buf.writeln(
"if (node.originalName == '${entry.key}') "
"{ node.module = '${entry.value}'; }",
);
}
}
if (yamlMap['include-transitive-objc-categories'] == false) {
buf.writeln('node.includeCategories = false;');
}
buf.writeln('},');
}
void _emitObjCProtocolVisitor(StringBuffer buf, YamlMap yamlMap) {
if (!yamlMap.containsKey('objc-protocols')) return;
final cfg = yamlMap['objc-protocols'] as Map?;
buf.writeln('objCProtocol: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: false);
_emitRenames(buf, cfg);
if (cfg?['module'] is Map) {
for (final entry in (cfg!['module'] as Map).entries) {
buf.writeln(
"if (node.originalName == '${entry.key}') "
"{ node.module = '${entry.value}'; }",
);
}
}
buf.writeln('},');
}
void _emitObjCCategoryVisitor(StringBuffer buf, YamlMap yamlMap) {
if (!yamlMap.containsKey('objc-categories')) return;
final cfg = yamlMap['objc-categories'] as Map?;
buf.writeln('objCCategory: (node) {');
_emitDeclarationFilter(buf, cfg, defaultInclude: false);
_emitRenames(buf, cfg);
buf.writeln('},');
}
void _emitCppClassVisitor(StringBuffer buf, YamlMap yamlMap) {
final cpp = yamlMap['cpp'] as Map?;
final cppClasses = cpp?['cpp-classes'] as Map?;
if (cppClasses == null) return;
buf.writeln('cppClass: (node) {');
_emitDeclarationFilter(buf, cppClasses, defaultInclude: false);
buf.writeln('},');
}
void _emitObjCMethodVisitor(StringBuffer buf, YamlMap yamlMap) {
final keys = [
('objc-interfaces', 'ObjCInterface'),
('objc-protocols', 'ObjCProtocol'),
('objc-categories', 'ObjCCategory'),
];
final hasAny = keys.any((k) {
final c = yamlMap[k.$1] as Map?;
return c?['member-filter'] != null || c?['member-rename'] != null;
});
if (!hasAny) return;
buf.writeln('objCMethod: (node) {');
buf.writeln('final parent = node.parent;');
for (final (key, type) in keys) {
final c = yamlMap[key] as Map?;
if (c == null) continue;
_emitMemberSection(
buf,
parentType: type,
filterMap: c['member-filter'] as Map?,
renameMap: c['member-rename'] as Map?,
nameProp: 'node.selector',
);
}
buf.writeln('},');
}
void _emitFieldVisitor(StringBuffer buf, YamlMap yamlMap) {
final keys = [('structs', 'Struct'), ('unions', 'Union')];
final hasAny = keys.any(
(k) => (yamlMap[k.$1] as Map?)?['member-rename'] != null,
);
if (!hasAny) return;
buf.writeln('field: (node) {');
buf.writeln('final parent = node.parent;');
for (final (key, type) in keys) {
final c = yamlMap[key] as Map?;
if (c == null) continue;
_emitMemberSection(
buf,
parentType: type,
renameMap: c['member-rename'] as Map?,
nameProp: 'node.originalName',
);
}
buf.writeln('},');
}
void _emitParamVisitor(StringBuffer buf, YamlMap yamlMap) {
final funcCfg = yamlMap['functions'] as Map?;
if (funcCfg?['member-rename'] == null) return;
buf.writeln('param: (node) {');
buf.writeln('final parent = node.parent;');
_emitMemberSection(
buf,
parentType: 'Func',
renameMap: funcCfg!['member-rename'] as Map?,
nameProp: 'node.originalName',
);
buf.writeln('},');
}
void _emitEnumConstantVisitor(StringBuffer buf, YamlMap yamlMap) {
final enumCfg = yamlMap['enums'] as Map?;
if (enumCfg?['member-rename'] == null) return;
buf.writeln('enumConstant: (node) {');
buf.writeln('final parent = node.parent;');
_emitMemberSection(
buf,
renameMap: enumCfg!['member-rename'] as Map?,
nameProp: 'node.originalName',
);
buf.writeln('},');
}
void _emitMemberSection(
StringBuffer buf, {
String? parentType,
Map<dynamic, dynamic>? filterMap,
Map<dynamic, dynamic>? renameMap,
required String nameProp,
}) {
final sBuf = StringBuffer();
if (filterMap != null) {
for (final entry in filterMap.entries) {
final parentDecl = entry.key.toString();
final filter = entry.value as Map;
final inc = (filter['include'] as List?)?.cast<String>();
final exc = (filter['exclude'] as List?)?.cast<String>();
final parentCond = _nameCond('parent.originalName', parentDecl);
if (exc != null &&
inc == null &&
exc.length == 1 &&
_isExact(exc.first)) {
sBuf.writeln(
'if ($parentCond && $nameProp == '
"'${exc.first}') { node.isIncluded = false; }",
);
} else {
final cond = _buildCondition(inc, exc, nameProp);
sBuf.writeln('if ($parentCond) { node.isIncluded = $cond; }');
}
}
}
if (renameMap != null) {
for (final entry in renameMap.entries) {
final parentDecl = entry.key.toString();
final renames = entry.value as Map;
final parentCond = _nameCond('parent.originalName', parentDecl);
for (final rentry in renames.entries) {
final from = rentry.key.toString();
final to = rentry.value.toString();
if (_isExact(from)) {
sBuf.writeln(
"if ($parentCond && $nameProp == '$from') { node.name = '$to'; }",
);
} else if (!to.contains(r'$')) {
final pat = _regexPat(from);
sBuf.writeln(
'if ($parentCond && RegExp(r\'^$pat\$\').hasMatch($nameProp)) '
"{ node.name = '$to'; }",
);
} else {
final pat = _regexPat(from);
sBuf.writeln('''
if ($parentCond) {
if (RegExp(r'^$pat\$').firstMatch($nameProp) case final match?) {
node.name = r'$to'.replaceAllMapped(RegExp(r'\\\$([0-9])'), (m) => match[int.parse(m[1]!)] ?? '');
}
}''');
}
}
}
}
if (sBuf.isEmpty) return;
if (parentType != null) {
buf.writeln('if (parent is $parentType) {\n$sBuf}');
} else {
buf.write(sBuf);
}
}
void _emitVisitors(StringBuffer buf, YamlMap yamlMap) {
final vBuf = StringBuffer();
_emitFuncVisitor(vBuf, yamlMap);
_emitStructVisitor(vBuf, yamlMap);
_emitUnionVisitor(vBuf, yamlMap);
_emitEnumVisitor(vBuf, yamlMap);
_emitUnnamedEnumVisitor(vBuf, yamlMap);
_emitGlobalVisitor(vBuf, yamlMap);
_emitMacroVisitor(vBuf, yamlMap);
_emitTypealiasVisitor(vBuf, yamlMap);
_emitObjCInterfaceVisitor(vBuf, yamlMap);
_emitObjCProtocolVisitor(vBuf, yamlMap);
_emitObjCCategoryVisitor(vBuf, yamlMap);
_emitCppClassVisitor(vBuf, yamlMap);
_emitObjCMethodVisitor(vBuf, yamlMap);
_emitFieldVisitor(vBuf, yamlMap);
_emitParamVisitor(vBuf, yamlMap);
_emitEnumConstantVisitor(vBuf, yamlMap);
if (vBuf.isNotEmpty) {
buf.writeln('visitors: [Visitor(');
buf.write(vBuf);
buf.writeln(')],');
}
}
String _buildCondition(
List<String>? includes,
List<String>? excludes,
String target,
) {
String? incCond;
if (includes != null && includes.isNotEmpty) {
final exact = includes.where(_isExact).toList();
final regex = includes.where((s) => !_isExact(s)).toList();
final parts = <String>[];
if (exact.length == 1) {
parts.add('$target == ${_str(exact.first)}');
} else if (exact.length > 1) {
parts.add('${_strSet(exact)}.contains($target)');
}
for (final r in regex) {
if (r == '.*') {
parts.add('true');
} else if (r.endsWith('.*') && _isExact(r.substring(0, r.length - 2))) {
parts.add('$target.startsWith(${_str(r.substring(0, r.length - 2))})');
} else {
final pat = _regexPat(r);
parts.add('RegExp(r\'^$pat\$\').hasMatch($target)');
}
}
incCond = parts.contains('true') ? 'true' : parts.join(' || ');
}
String? excCond;
if (excludes != null && excludes.isNotEmpty) {
final exact = excludes.where(_isExact).toList();
final regex = excludes.where((s) => !_isExact(s)).toList();
final parts = <String>[];
if (exact.length == 1) {
parts.add('$target != ${_str(exact.first)}');
} else if (exact.length > 1) {
parts.add('!${_strSet(exact)}.contains($target)');
}
for (final r in regex) {
if (r.endsWith('.*') && _isExact(r.substring(0, r.length - 2))) {
parts.add('!$target.startsWith(${_str(r.substring(0, r.length - 2))})');
} else {
final pat = _regexPat(r);
parts.add('!RegExp(r\'^$pat\$\').hasMatch($target)');
}
}
excCond = parts.join(' && ');
}
if (incCond != null && excCond != null) {
if (incCond == 'true') return excCond;
final parenInc = incCond.contains(' || ') ? '($incCond)' : incCond;
return '$excCond && $parenInc';
}
return incCond ?? excCond ?? 'true';
}