topLevelDeclarations function

List<({String kind, String name})> topLevelDeclarations(
  1. String source
)

The top-level declarations of source, public and private, in order.

A scanner rather than a line pattern, because the declarations a line pattern misses are exactly the awkward ones: a return type on the line above its function, a record return type, a named-parameter brace that is not a body, a brace inside a string. Comments go first; strings, including interpolations, are skipped as units; a body is skipped by balance; what is left at the top level is a header, classified by its keyword.

Implementation

List<({String name, String kind})> topLevelDeclarations(String source) {
  final s = dartCodeWithoutComments(source);
  final found = <({String name, String kind})>[];

  // Index just past the bracket that closes the one opening at its argument;
  // declared first because a string's interpolation and a bracket's string
  // each skip the other.
  late final int Function(int) skipBalanced;

  // Index just past the string literal opening at [i].
  int skipString(int i) {
    final raw = i > 0 && (s[i - 1] == 'r' || s[i - 1] == 'R');
    final quote = s[i];
    final triple = s.startsWith(quote * 3, i);
    final terminator = triple ? quote * 3 : quote;
    i += terminator.length;
    while (i < s.length && !s.startsWith(terminator, i)) {
      if (!raw && s[i] == r'\') {
        i += 2;
        continue;
      }
      if (!raw && s.startsWith(r'${', i)) {
        i = skipBalanced(i + 1);
        continue;
      }
      i++;
    }
    return i + terminator.length;
  }

  skipBalanced = (int i) {
    var depth = 0;
    while (i < s.length) {
      final c = s[i];
      if (c == "'" || c == '"') {
        i = skipString(i);
        continue;
      }
      if (c == '{' || c == '(' || c == '[') depth++;
      if (c == '}' || c == ')' || c == ']') {
        depth--;
        if (depth == 0) return i + 1;
      }
      i++;
    }
    return i;
  };

  // Index just past the `;` that ends the expression starting at [i].
  int skipToSemicolon(int i) {
    while (i < s.length) {
      final c = s[i];
      if (c == "'" || c == '"') {
        i = skipString(i);
      } else if (c == '{' || c == '(' || c == '[') {
        i = skipBalanced(i);
      } else if (c == ';') {
        return i + 1;
      } else {
        i++;
      }
    }
    return i;
  }

  final typeHeader = RegExp(
    r'^(?:(?:abstract|base|final|sealed|interface|mixin)\s+)*class\s+(\w+)'
    r'|^(?:base\s+)?mixin\s+(\w+)'
    r'|^enum\s+(\w+)'
    r'|^extension\s+type\s+(?:const\s+)?(\w+)'
    r'|^extension\s+(?!on\b)(\w+)'
    r'|^typedef\s+(\w+)',
  );
  const typeKinds = [
    'class', 'mixin', 'enum', 'extension', 'extension', 'typedef', //
  ];
  final directive = RegExp(r'^(?:library|import|export|part)\b');
  final accessor = RegExp(r'\b(get|set)\s+(\w+)\s*$');
  // A generic function's own type parameters sit between its name and the
  // list: `List<T> merge<T>(`. They may nest one level (`<K, List<V>>`) and
  // never span a `>` — so a return type's arguments are not read as the
  // function's and its type name as the function's name.
  final parameterList = RegExp(
    r'([A-Za-z_$][\w$]*)\s*(?:<(?:[^()<>]|<[^()<>]*>)*>)?\s*\(',
  );

  String withoutAnnotations(String header) {
    var h = header.trimLeft();
    while (h.startsWith('@')) {
      final name = RegExp(r'^@[\w.]+').firstMatch(h)!;
      h = h.substring(name.end).trimLeft();
      if (h.startsWith('(')) {
        var depth = 0;
        var j = 0;
        for (; j < h.length; j++) {
          if (h[j] == '(') depth++;
          if (h[j] == ')' && --depth == 0) break;
        }
        h = h.substring(j + 1).trimLeft();
      }
    }
    return h.replaceAll(RegExp(r'\s+'), ' ').trim();
  }

  void classify(String rawHeader, {required bool initializer}) {
    final header = withoutAnnotations(rawHeader);
    if (header.isEmpty || directive.hasMatch(header)) return;
    final type = typeHeader.firstMatch(header);
    if (type != null) {
      for (var g = 1; g <= typeKinds.length; g++) {
        final name = type.group(g);
        if (name != null) {
          found.add((name: name, kind: typeKinds[g - 1]));
          return;
        }
      }
    }
    if (initializer) return; // a variable
    // The parameter list is the first `name(` at bracket depth 0 that is not
    // a function type — a record return type opens with a parenthesis too.
    RegExpMatch? call;
    for (final m in parameterList.allMatches(header)) {
      final before = header.substring(0, m.start);
      final depth =
          '('.allMatches(before).length - ')'.allMatches(before).length;
      if (depth == 0 && m.group(1) != 'Function') {
        call = m;
        break;
      }
    }
    final signature = call == null
        ? header
        : header.substring(0, call.start + call.group(1)!.length);
    final asAccessor = accessor.firstMatch(signature);
    if (asAccessor != null) {
      found.add((
        name: asAccessor.group(2)!,
        kind: asAccessor.group(1) == 'get' ? 'getter' : 'setter',
      ));
      return;
    }
    if (call != null) found.add((name: call.group(1)!, kind: 'function'));
  }

  final header = StringBuffer();
  var i = 0;
  while (i < s.length) {
    final c = s[i];
    if (c == "'" || c == '"') {
      final end = skipString(i);
      header.write(s.substring(i, end));
      i = end;
    } else if (c == '(' || c == '[') {
      final end = skipBalanced(i);
      header.write(s.substring(i, end));
      i = end;
    } else if (c == '{') {
      classify(header.toString(), initializer: false);
      header.clear();
      i = skipBalanced(i);
    } else if (c == ';') {
      classify(header.toString(), initializer: false);
      header.clear();
      i++;
    } else if (s.startsWith('=>', i)) {
      classify(header.toString(), initializer: false);
      header.clear();
      i = skipToSemicolon(i + 2);
    } else if (c == '=') {
      classify(header.toString(), initializer: true);
      header.clear();
      i = skipToSemicolon(i + 1);
    } else {
      header.write(c);
      i++;
    }
  }
  return found;
}