blob: 39a9b90e38a6a54886698db4cdb6bb91a9cff8ca [file] [edit]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// Dart source directive parser and token rewriting utilities.
library;
import 'package:_fe_analyzer_shared/src/parser/experimental_features.dart';
import 'package:_fe_analyzer_shared/src/parser/parser.dart';
import 'package:_fe_analyzer_shared/src/parser/quote.dart';
import 'package:_fe_analyzer_shared/src/scanner/scanner.dart';
/// The kind of top-level directive.
enum DirectiveKind { import, export, part, partOf }
/// Information about a directive occurrence in a Dart source file.
class DirectiveOccurrence {
final DirectiveKind kind;
final String uri;
final int offset;
final int length;
final String rawLexeme;
DirectiveOccurrence({
required this.kind,
required this.uri,
required this.offset,
required this.length,
required this.rawLexeme,
});
bool get isImport => kind == .import;
bool get isExport => kind == .export;
bool get isPart => kind == .part;
bool get isPartOf => kind == .partOf;
@override
String toString() =>
'DirectiveOccurrence($kind: $uri, offset: $offset, len: $length)';
}
/// Token listener to extract directive URIs with exact token offsets.
///
/// Inspired by pkg/front_end/lib/src/source/directive_listener.dart, this
/// listener preserves exact offsets and lengths of directive string literals
/// for rewriting.
class DirectiveTokenListener extends Listener {
final List<DirectiveOccurrence> occurrences = [];
bool _inDirective = false;
DirectiveKind? _currentKind;
bool _uriCaptured = false;
bool _inConditionalUri = false;
DirectiveOccurrence? _lastConditionalString;
@override
void beginImport(Token importKeyword) {
_inDirective = true;
_currentKind = .import;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void endImport(Token? importKeyword, Token? semicolon) {
_inDirective = false;
_currentKind = null;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void beginExport(Token token) {
_inDirective = true;
_currentKind = .export;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void endExport(Token exportKeyword, Token semicolon) {
_inDirective = false;
_currentKind = null;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void beginPart(Token token) {
_inDirective = true;
_currentKind = .part;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void endPart(Token partKeyword, Token semicolon) {
_inDirective = false;
_currentKind = null;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void beginPartOf(Token token) {
_inDirective = true;
_currentKind = .partOf;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void endPartOf(
Token partKeyword,
Token ofKeyword,
Token semicolon,
bool hasName,
) {
_inDirective = false;
_currentKind = null;
_uriCaptured = false;
_inConditionalUri = false;
}
@override
void beginConditionalUri(Token ifKeyword) {
_inConditionalUri = true;
_lastConditionalString = null;
}
@override
void endConditionalUri(Token ifKeyword, Token leftParen, Token? equalSign) {
if (_lastConditionalString != null) {
occurrences.add(_lastConditionalString!);
_lastConditionalString = null;
}
_inConditionalUri = false;
}
@override
void beginLiteralString(Token token) {
if (!_inDirective ||
_currentKind == null ||
token.isSynthetic ||
token.length == 0) {
return;
}
final lexeme = token.lexeme;
final unescaped = unescapeString(lexeme, token, this);
final occurrence = DirectiveOccurrence(
kind: _currentKind!,
uri: unescaped,
offset: token.offset,
length: token.length,
rawLexeme: lexeme,
);
if (_inConditionalUri) {
// In conditional URIs, the final literal string in the construct is the
// target URI.
_lastConditionalString = occurrence;
} else if (!_uriCaptured) {
occurrences.add(occurrence);
_uriCaptured = true;
}
}
}
/// Extracts all directive occurrences from Dart source [content].
List<DirectiveOccurrence> extractDirectives(String content) {
final listener = DirectiveTokenListener();
final parser = TopLevelParser(
listener,
experimentalFeatures: const DefaultExperimentalFeatures(),
);
final tokens = scanString(content).tokens;
parser.parseUnit(tokens);
return listener.occurrences;
}
/// Rewrites a literal string token lexeme with [newUri].
///
/// Preserves the original quotation format and raw string prefix.
String rewriteLexeme(String rawLexeme, String newUri) {
final isRaw = rawLexeme.startsWith('r') || rawLexeme.startsWith('R');
final withoutRaw = isRaw ? rawLexeme.substring(1) : rawLexeme;
final quote = withoutRaw.startsWith("'''") || withoutRaw.startsWith('"""')
? withoutRaw.substring(0, 3)
: (withoutRaw.isNotEmpty ? withoutRaw.substring(0, 1) : "'");
final prefix = isRaw ? rawLexeme.substring(0, 1) : '';
return '$prefix$quote$newUri$quote';
}
/// Rewrites the token for [directive] in [content] with [newUri].
String rewriteDirective(
String content,
DirectiveOccurrence directive,
Uri newUri,
) => content.replaceRange(
directive.offset,
directive.offset + directive.length,
rewriteLexeme(directive.rawLexeme, newUri.toString()),
);