discoverEntries function
Implementation
Future<List<PreviewEntry>> discoverEntries(String projectRoot) async {
final packageName = packageNameOf(projectRoot);
if (packageName == null) return const [];
final libDir = Directory(p.join(projectRoot, 'lib'));
if (!libDir.existsSync()) return const [];
final files =
libDir
.listSync(recursive: true)
.whereType<File>()
.where((f) => f.path.endsWith('.dart') && !f.path.endsWith('.g.dart'))
.map((f) => p.normalize(f.absolute.path))
.toList()
..sort();
final collection = AnalysisContextCollection(
includedPaths: [p.normalize(libDir.absolute.path)],
);
// RESOLUTION CANARY. Every verdict below depends on the element model:
// if the project does not resolve (no `pub get`, a broken
// package_config, an SDK mismatch), annotations classify as nothing and
// supertypes bind to nothing — discovery would silently return ZERO
// entries and the generated suite would pass while proving nothing. A
// gate must never fail open, so refuse instead.
if (files.isNotEmpty) {
final session = collection.contextFor(files.first).currentSession;
final flutter = await session.getLibraryByUri(
'package:flutter/widgets.dart',
);
if (flutter is! LibraryElementResult) {
throw StateError(
'cannot resolve package:flutter/widgets.dart in $projectRoot — '
'run `flutter pub get` there first (and make sure the Flutter SDK '
'on PATH is the one the project resolves against).',
);
}
}
final entries = <PreviewEntry>[];
for (final file in files) {
final session = collection.contextFor(file).currentSession;
final resolved = await session.getResolvedUnit(file);
if (resolved is! ResolvedUnitResult) {
throw StateError(
'could not resolve ${p.relative(file, from: projectRoot)} '
'(${resolved.runtimeType}) — greenroom refuses to generate a '
'partial suite.',
);
}
final unit = resolved.unit;
if (unit.directives.whereType<PartOfDirective>().isNotEmpty) continue;
final rel = p.relative(file, from: libDir.absolute.path);
final importUri = 'package:$packageName/${p.split(rel).join('/')}';
void addAll(
String expr,
List<Annotation> metadata, {
bool isBuilder = false,
}) {
var i = 0;
for (final a in metadata) {
final c = _classify(a);
if (c == null) continue;
final suffix = i == 0 ? '' : '#$i';
final id = '$rel::$expr$suffix';
i++;
final name = a.elementAnnotation
?.computeConstantValue()
?.getField('name')
?.toStringValue();
final rewritten = _reEmitAnnotation(a, c.type, resolved.content);
entries.add(
PreviewEntry(
id: id,
importUri: importUri,
expr: expr,
kind: 'preview',
name: name,
annoCode: rewritten.blocked == null ? rewritten.code : null,
isMulti: c.multi,
isBuilder: isBuilder,
note: rewritten.blocked == null
? null
: 'annotation on $id cannot be re-emitted '
'(${rewritten.blocked}) — mounted with NO preview '
'fields applied',
),
);
}
}
/// `@Preview` members may return `Widget` OR `WidgetBuilder`.
bool returnsBuilder(DartType? type) =>
type is FunctionType && type.returnType.element?.name == 'Widget';
for (final decl in unit.declarations) {
if (decl is FunctionDeclaration) {
addAll(
'${decl.name.lexeme}()',
decl.metadata.toList(),
isBuilder: returnsBuilder(decl.declaredFragment?.element.returnType),
);
continue;
}
if (decl is! ClassDeclaration) continue;
final cls = decl.declaredFragment?.element;
if (cls == null) continue;
final className = '${cls.name}';
if (className.startsWith('_')) continue;
final before = entries.length;
for (final m in decl.body.members.whereType<MethodDeclaration>()) {
if (!m.isStatic) continue;
addAll(
'$className.${m.name.lexeme}()',
m.metadata.toList(),
isBuilder: returnsBuilder(m.declaredFragment?.element.returnType),
);
}
for (final c in decl.body.members.whereType<ConstructorDeclaration>()) {
if (c.metadata.isEmpty) continue;
if (c.parameters.parameters.any((param) => param.isRequired)) {
continue;
}
final ctor = c.name == null ? '' : '.${c.name!.lexeme}';
addAll('$className$ctor()', c.metadata.toList());
}
final hasOwnEntry = entries.length > before;
if (hasOwnEntry) continue;
// Bare-mount candidate — else the obligation. Transitive bases via
// the element model; InheritedWidgets are supplies, not subjects.
final isWidget =
!cls.isAbstract &&
cls.allSupertypes.any((s) => s.element.name == 'Widget');
if (!isWidget) continue;
if (cls.allSupertypes.any((s) => s.element.name == 'InheritedWidget')) {
continue;
}
final defaultCtors = [
for (final ctor in cls.constructors)
if (ctor.name == 'new' || (ctor.name ?? '') == '') ctor,
];
final mountable =
defaultCtors.isNotEmpty &&
!defaultCtors.first.formalParameters.any(
(param) => param.isRequiredNamed || param.isRequiredPositional,
);
if (mountable) {
entries.add(
PreviewEntry(
id: '$rel::$className()',
importUri: importUri,
expr: '$className()',
kind: 'bare',
),
);
} else {
entries.add(
PreviewEntry(
id: '$rel::$className',
importUri: importUri,
expr: '',
kind: 'missing',
className: className,
),
);
}
}
}
return entries;
}