parseExportFiles method

Future<Map<String, ExportInfo>> parseExportFiles(
  1. List<String> barrelFiles, {
  2. Set<String>? visited,
  3. bool followAllReExports = true,
  4. List<String>? skipReExports,
  5. List<String>? followReExports,
  6. bool isTopLevel = true,
  7. String? originBarrelUri,
  8. List<String>? parentShowClause,
  9. List<String>? parentHideClause,
})

Parses export barrel files to extract the list of exported source files.

Scans the given barrel files (e.g., lib/tom_build.dart) and extracts all export '...' directives to build a list of source files to bridge. Recursively follows re-exported barrel files.

followAllReExports - If true (default), follows all external package re-exports except those in skipReExports. If false, only follows packages in followReExports. skipReExports - List of package names to skip when followAllReExports is true. followReExports - List of external package names to follow when followAllReExports is false. originBarrelUri - The top-level barrel URI that originated this export chain. Used to track which barrel file each source file came from. parentShowClause - Show clause from parent export, to be merged with nested exports. parentHideClause - Hide clause from parent export, to be merged with nested exports.

Returns a map of source file paths to their export info (hide/show clauses and barrel origin).

Implementation

Future<Map<String, ExportInfo>> parseExportFiles(
  List<String> barrelFiles, {
  Set<String>? visited,
  bool followAllReExports = true,
  List<String>? skipReExports,
  List<String>? followReExports,
  bool isTopLevel = true,
  String? originBarrelUri,
  List<String>? parentShowClause,
  List<String>? parentHideClause,
}) async {
  visited ??= <String>{};
  skipReExports ??= const [];
  followReExports ??= const [];
  final exports = <String, ExportInfo>{};
  final exportPattern = RegExp(
    r'''export\s+['"]([^'"]+)['"]\s*(hide\s+[^;]+|show\s+[^;]+)?;''',
    multiLine: true,
  );

  for (final barrelPath in barrelFiles) {
    // Normalize path for comparison
    final normalizedPath = p.normalize(barrelPath);
    // For top-level barrel files, allow re-processing even if already visited
    // during recursive export chain from another barrel. This ensures each
    // top-level barrel gets its own barrelUri assigned correctly.
    // Only skip for recursive (non-top-level) calls to prevent infinite loops.
    if (!isTopLevel && visited.contains(normalizedPath)) {
      continue;
    }
    visited.add(normalizedPath);

    final file = File(normalizedPath);
    if (!file.existsSync()) {
      if (verbose) print('Warning: Barrel file not found: $normalizedPath');
      continue;
    }

    // Determine the barrel URI for this barrel file
    // For top-level barrels, compute it from the path; for nested, use originBarrelUri
    String? currentBarrelUri = originBarrelUri;
    if (isTopLevel) {
      // Convert file path to package URI for the barrel
      currentBarrelUri = _getPackageUri(normalizedPath);
    }

    // Add the barrel file itself to exports (it may contain its own declarations
    // like top-level getters, functions, variables, classes, or enums)
    // Only add for top-level barrel files (not nested barrels from recursive calls)
    // Nested barrels will have their restrictions handled by the parent export statement
    if (isTopLevel) {
      final existingBarrelInfo = exports[normalizedPath];
      if (existingBarrelInfo == null) {
        exports[normalizedPath] = ExportInfo(
          sourcePath: normalizedPath,
          hideClause: null,
          showClause: null,
          barrelUri: currentBarrelUri,
        );
      }
    }

    final content = await file.readAsString();

    for (final match in exportPattern.allMatches(content)) {
      final exportPath = match.group(1)!;
      final hideShow = match.group(2)?.trim();

      // Handle package: exports
      String absolutePath;
      if (exportPath.startsWith('package:')) {
        // Extract package name from export path
        final packageMatch = RegExp(
          r'^package:([^/]+)/(.+)$',
        ).firstMatch(exportPath);
        if (packageMatch == null) continue;

        final exportPackageName = packageMatch.group(1)!;
        final exportRelativePath = packageMatch.group(2)!;

        // Check if this is the current package
        if (packageName != null && exportPackageName == packageName) {
          // GEN-077: Check skipReExports for same-package re-exports.
          // When e.g. widgets/basic.dart has
          //   export 'package:flutter/animation.dart';
          // — the full URI must be checked against skipReExports because
          // both sides share the same package name ('flutter').
          if (skipReExports.contains(exportPath)) {
            if (verbose) {
              print(
                'Skipping same-package re-export $exportPath (in skipReExports)',
              );
            }
            continue;
          }
          // Convert package: to relative path for current package
          absolutePath = '$workspacePath/lib/$exportRelativePath';
        } else {
          // Determine if we should follow this external package's re-exports
          bool shouldFollow;
          if (followAllReExports) {
            // Follow all except those in skipReExports.
            // GEN-077: Match both full URI (package:foo/bar.dart)
            // and bare package name (foo) for backwards compatibility.
            shouldFollow =
                !skipReExports.contains(exportPackageName) &&
                !skipReExports.contains(exportPath);
          } else {
            // Only follow those explicitly listed in followReExports
            shouldFollow = followReExports.contains(exportPackageName);
          }

          if (shouldFollow) {
            // Follow re-exports from external package
            final externalPackagePath = await _resolvePackagePath(
              exportPackageName,
            );
            if (externalPackagePath == null) {
              if (verbose) {
                print(
                  'Warning: Could not resolve package path for $exportPackageName',
                );
              }
              continue;
            }
            absolutePath = '$externalPackagePath/lib/$exportRelativePath';
          } else {
            // External package not configured to be followed, skip
            if (verbose) {
              print(
                'Skipping re-export from $exportPackageName (not configured to follow)',
              );
            }
            continue;
          }
        }
      } else if (exportPath.startsWith('dart:')) {
        continue; // Skip dart: exports
      } else {
        // Relative path
        final barrelDir = p.dirname(normalizedPath);
        absolutePath = p.normalize(p.join(barrelDir, exportPath));
      }

      // Check if this is another barrel file (re-exports other files)
      final exportFile = File(absolutePath);
      if (exportFile.existsSync()) {
        final exportContent = await exportFile.readAsString();
        if (exportPattern.hasMatch(exportContent)) {
          // Parse the current export's show/hide clause
          List<String>? currentShowClause;
          List<String>? currentHideClause;
          if (hideShow?.startsWith('show') == true) {
            currentShowClause = hideShow!
                .substring(5)
                .split(',')
                .map((s) => s.trim())
                .toList();
          } else if (hideShow?.startsWith('hide') == true) {
            currentHideClause = hideShow!
                .substring(5)
                .split(',')
                .map((s) => s.trim())
                .toList();
          }

          // Merge current clause with parent clause for propagation
          final mergedShowClause = _mergeShowClauses(
            parentShowClause,
            currentShowClause,
          );
          final mergedHideClause = _mergeHideClauses(
            parentHideClause,
            currentHideClause,
          );

          // This file is a barrel - recursively parse it
          final nestedExports = await parseExportFiles(
            [absolutePath],
            visited: visited,
            followAllReExports: followAllReExports,
            skipReExports: skipReExports,
            followReExports: followReExports,
            isTopLevel: false,
            originBarrelUri: currentBarrelUri,
            parentShowClause: mergedShowClause,
            parentHideClause: mergedHideClause,
          );
          // Add nested exports with parent clauses applied
          for (final entry in nestedExports.entries) {
            final existingNested = exports[entry.key];

            // Merge the nested export with parent show/hide clauses
            final mergedEntry = entry.value.mergeWithParent(
              parentShowClause: parentShowClause,
              parentHideClause: parentHideClause,
            );

            if (existingNested == null) {
              // No existing entry - set directly
              exports[entry.key] = mergedEntry.barrelUri != null
                  ? mergedEntry
                  : mergedEntry.copyWith(barrelUri: currentBarrelUri);
            } else if (existingNested.showClause == null &&
                existingNested.hideClause == null) {
              // Existing is fully permissive (no restrictions) - keep it
            } else if (mergedEntry.showClause != null &&
                existingNested.showClause != null) {
              // GEN-070: Multiple export chains to the same source file are
              // additive in Dart. Union the show clauses so symbols visible
              // through ANY chain are included.
              final unionShow = {
                ...existingNested.showClause!,
                ...mergedEntry.showClause!,
              }.toList();
              exports[entry.key] = existingNested.copyWith(
                showClause: unionShow,
              );
            } else {
              // Default: new entry overrides (e.g., new entry is more permissive)
              exports[entry.key] = mergedEntry.barrelUri != null
                  ? mergedEntry
                  : mergedEntry.copyWith(barrelUri: currentBarrelUri);
            }
          }
        }
        // Always add the file itself to exports (it may contain its own code)
        // even if it's a barrel file that re-exports other files
        // BUT: Don't overwrite an existing entry if:
        // 1. It's more permissive (no show/hide clause beats having a show/hide clause)
        // 2. It's from the same package as the source file (prefer same-package barrel)
        final existingInfo = exports[absolutePath];

        // Check if this barrel is from the same package as the source file
        final sourcePackage = _extractPackageFromPath(absolutePath);
        final currentBarrelPackage = currentBarrelUri != null
            ? _extractPackageFromUri(currentBarrelUri)
            : null;
        final existingBarrelPackage = existingInfo?.barrelUri != null
            ? _extractPackageFromUri(existingInfo!.barrelUri!)
            : null;

        final isSamePackageBarrel =
            sourcePackage != null && currentBarrelPackage == sourcePackage;
        final existingIsSamePackage =
            sourcePackage != null && existingBarrelPackage == sourcePackage;

        final isExistingMorePermissive =
            existingInfo != null &&
            existingInfo.showClause == null &&
            existingInfo.hideClause == null;

        // GEN-072: Check if current export is more permissive (no restrictions).
        // This is true when the export statement has no show/hide clause AND
        // there are no parent restrictions from the barrel chain above.
        final isCurrentMorePermissive =
            hideShow == null &&
            parentShowClause == null &&
            parentHideClause == null;

        // Override if:
        // - No existing entry
        // - Current is more permissive than existing (GEN-072: prefer permissive)
        // - This barrel is same-package and existing is not
        // - Existing is not more permissive AND this isn't less preferred
        final shouldOverride =
            existingInfo == null ||
            (isCurrentMorePermissive && !isExistingMorePermissive) ||
            (isSamePackageBarrel && !existingIsSamePackage) ||
            (!isExistingMorePermissive && !existingIsSamePackage);

        if (shouldOverride) {
          // Parse current export's show/hide clause
          List<String>? currentHide;
          List<String>? currentShow;
          if (hideShow?.startsWith('hide') == true) {
            currentHide = hideShow!
                .substring(5)
                .split(',')
                .map((s) => s.trim())
                .toList();
          } else if (hideShow?.startsWith('show') == true) {
            currentShow = hideShow!
                .substring(5)
                .split(',')
                .map((s) => s.trim())
                .toList();
          }

          // Merge with parent clauses
          final mergedShow = _mergeShowClauses(parentShowClause, currentShow);
          final mergedHide = _mergeHideClauses(parentHideClause, currentHide);

          // If both show and hide exist, apply hide to show
          List<String>? finalShow = mergedShow;
          List<String>? finalHide = mergedHide;
          if (finalShow != null && finalHide != null) {
            finalShow = finalShow
                .where((s) => !finalHide!.contains(s))
                .toList();
            finalHide = null;
          }

          // GEN-070: If existing entry has a show clause and we have one too,
          // union them (multiple export chains are additive in Dart).
          if (existingInfo != null &&
              existingInfo.showClause != null &&
              finalShow != null) {
            finalShow = {...existingInfo.showClause!, ...finalShow}.toList();
          }

          exports[absolutePath] = ExportInfo(
            sourcePath: absolutePath,
            hideClause: finalHide,
            showClause: finalShow,
            barrelUri: currentBarrelUri,
          );
        }
      }
    }

    // GEN-070: Remove from visited after processing completes.
    // This converts the visited set from "ever visited" to a recursion-stack,
    // preventing cycles (A->B->A) but allowing the same barrel to be reached
    // through different export chains with different show/hide clauses.
    // Top-level barrels skip the visited check anyway, so only remove for
    // non-top-level (recursive) calls.
    if (!isTopLevel) {
      visited.remove(normalizedPath);
    }
  }

  return exports;
}