discoverEntries function

Future<List<PreviewEntry>> discoverEntries(
  1. String projectRoot
)

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