purgeStaleCppKotlinRegistrations function

void purgeStaleCppKotlinRegistrations(
  1. List<ModuleInfo> cppModules, {
  2. String baseDir = '.',
})

Removes stale <Module>JniBridge.register(...) calls from Plugin.kt for modules that have been converted to NativeImpl.cpp.

When a user switches android: NativeImpl.kotlinAndroidNativeImpl.cpp (or any C++ variant), the old registration call is left as dead code that causes a Kotlin "Unresolved reference" compile error. This function finds and removes those stale calls automatically on every nitrogen link run.

Implementation

void purgeStaleCppKotlinRegistrations(
  List<ModuleInfo> cppModules, {
  String baseDir = '.',
}) {
  if (cppModules.isEmpty) return;
  final kotlinDir = Directory(
    p.join(baseDir, 'android', 'src', 'main', 'kotlin'),
  );
  if (!kotlinDir.existsSync()) return;
  final pluginFiles = kotlinDir
      .listSync(recursive: true, followLinks: false)
      .whereType<File>()
      .where((f) => !f.path.contains('.symlinks'))
      .where((f) => f.path.endsWith('Plugin.kt'))
      .toList();
  if (pluginFiles.isEmpty) return;
  final pluginFile = pluginFiles.first;
  var content = pluginFile.readAsStringSync();
  bool modified = false;

  for (final m in cppModules) {
    // Match: <Module>JniBridge.register(<anything>) OR .registerFactory(...)
    // Anchored to line start (^ with multiLine): without it, a module whose
    // class name is a SUFFIX of another's (e.g. cpp module `Present` vs
    // Kotlin module `WebgpuPresent`) would match inside
    // `WebgpuPresentJniBridge.register(...)` and corrupt that line.
    final stalePattern = RegExp(
      r'^[ \t]*' + RegExp.escape('${m.module}JniBridge') + r'\.register\w*\(.*\)[ \t]*\r?\n?',
      multiLine: true,
    );
    if (stalePattern.hasMatch(content)) {
      content = content.replaceAll(stalePattern, '');
      modified = true;
    }
  }

  // Clean up orphaned imports for the removed JniBridge class — ONLY when
  // nothing else in the file still references it. An all-cpp Android module
  // still emits a JniBridge class (lifecycle hooks such as
  // onActivityAttached), and a user's ActivityAware plugin legitimately
  // calls it; removing a still-used import breaks compileDebugKotlin with
  // "Unresolved reference" (issue #16, reopened).
  for (final m in cppModules) {
    // `\.` before the class and a non-identifier boundary after it, so the
    // import of a longer-named sibling (WebgpuPresentJniBridge when purging
    // Present) is never mistaken for this module's import.
    final importPattern = RegExp(
      r'^import [^\n]+?\.' + RegExp.escape('${m.module}JniBridge') + r'(?![A-Za-z0-9_])[^\n]*\n?',
      multiLine: true,
    );
    if (!importPattern.hasMatch(content)) continue;
    final withoutImports = content.replaceAll(importPattern, '');
    // Identifier-boundary usage check: `PresentJniBridge` inside
    // `WebgpuPresentJniBridge` must not count as a remaining usage.
    final usagePattern = RegExp(
      r'(?<![A-Za-z0-9_.])' + RegExp.escape('${m.module}JniBridge') + r'(?![A-Za-z0-9_])',
    );
    if (!usagePattern.hasMatch(withoutImports)) {
      content = withoutImports;
      modified = true;
    }
  }

  if (modified) pluginFile.writeAsStringSync(content);
}