detectManagedContentIssues function

List<ManagedContentIssue> detectManagedContentIssues({
  1. String baseDir = '.',
})

Scans Plugin.kt and Plugin.swift for managed sections (JniBridge import, register() call, Registry.register) that are expected for non-cpp modules but are currently missing. Returns each gap as a ManagedContentIssue.

Called before the link TUI starts so the user can confirm re-injection.

Implementation

List<ManagedContentIssue> detectManagedContentIssues({String baseDir = '.'}) {
  final issues = <ManagedContentIssue>[];

  final libDir = Directory(p.join(baseDir, 'lib'));
  if (!libDir.existsSync()) return issues;
  final allSpecFiles = libDir
      .listSync(recursive: true)
      .whereType<File>()
      .where((f) => f.path.endsWith('.native.dart'))
      .toList();
  if (allSpecFiles.isEmpty) return issues;

  // For Android Plugin.kt: a module needs JniBridge registration when it does NOT
  // use a native C++ impl on android/linux (isNativeCppModule). A module like
  // `benchmark` (android: kotlin, windows: cpp) is correctly included here because
  // isNativeCppModule checks android/linux only — isCppModule (broad) would
  // falsely exclude it due to the windows: cpp entry.
  final androidSpecFiles = allSpecFiles
      .where((f) => !isNativeCppModule(f))
      .toList();

  // For iOS Plugin.swift: a module needs Registry.register when it does NOT use
  // NativeImpl.cpp specifically on iOS (mixed ios:swift/macos:cpp still needs iOS registration).
  final iosSpecFiles = allSpecFiles.where((f) => !isIosCppModule(f)).toList();

  // ── Android: Plugin.kt ────────────────────────────────────────────────────
  final ktDir = Directory(p.join(baseDir, 'android', 'src', 'main', 'kotlin'));
  if (ktDir.existsSync()) {
    final pluginFiles = ktDir
        .listSync(recursive: true)
        .whereType<File>()
        .where((f) => f.path.endsWith('Plugin.kt'))
        .toList();
    if (pluginFiles.isNotEmpty) {
      final kt = pluginFiles.first.readAsStringSync();
      final ktPath = p.relative(pluginFiles.first.path, from: baseDir);
      for (final specFile in androidSpecFiles) {
        final stem = p
            .basename(specFile.path)
            .replaceAll(RegExp(r'\.native\.dart$'), '');
        final lib = (extractLibNameFromSpec(specFile) ?? stem).replaceAll(
          '-',
          '_',
        );
        final moduleMatch = RegExp(
          r'abstract class (\w+) extends HybridObject',
        ).firstMatch(specFile.readAsStringSync());
        final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
        final importLine = 'import nitro.${lib}_module.${moduleName}JniBridge';
        final registerCall = '${moduleName}JniBridge.register(';
        if (!kt.contains(importLine)) {
          issues.add(
            ManagedContentIssue(
              file: ktPath,
              description: 'Missing import: $importLine',
            ),
          );
        }
        if (!kt.contains(registerCall)) {
          issues.add(
            ManagedContentIssue(
              file: ktPath,
              description:
                  'Missing registration: ${moduleName}JniBridge.register(${moduleName}Impl(...))',
            ),
          );
        }
      }
    }
  }

  // ── iOS: Plugin.swift ─────────────────────────────────────────────────────
  final iosDir = Directory(p.join(baseDir, 'ios'));
  if (iosDir.existsSync()) {
    final swiftFiles = iosDir
        .listSync(recursive: true, followLinks: false)
        .whereType<File>()
        .where(
          (f) =>
              !f.path.contains('.symlinks') && f.path.endsWith('Plugin.swift'),
        )
        .toList();
    if (swiftFiles.isNotEmpty) {
      final swift = swiftFiles.first.readAsStringSync();
      final swiftPath = p.relative(swiftFiles.first.path, from: baseDir);
      for (final specFile in iosSpecFiles) {
        final stem = p
            .basename(specFile.path)
            .replaceAll(RegExp(r'\.native\.dart$'), '');
        final moduleMatch = RegExp(
          r'abstract class (\w+) extends HybridObject',
        ).firstMatch(specFile.readAsStringSync());
        final moduleName = moduleMatch?.group(1) ?? _toPascalCase(stem);
        if (!swift.contains('${moduleName}Registry.register(')) {
          issues.add(
            ManagedContentIssue(
              file: swiftPath,
              description:
                  'Missing registration: ${moduleName}Registry.register(${moduleName}ModuleImpl())',
            ),
          );
        }
      }
    }
  }

  return issues;
}