parseExportFiles method
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;
}