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