mirror of
https://github.com/dart-lang/sdk
synced 2024-09-16 03:47:13 +00:00
[js_ast] Add annotations facility
'Annotations' allow client information to be attached to js_ast nodes. I will be using this facility to embed resource identifers in the generated JavaScript AST to generate a map from files to the resource identifiers that they contain. Change-Id: Id9012b303de0d2b3848a635bc34747f8c5101236 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/281320 Reviewed-by: Nate Biggs <natebiggs@google.com> Commit-Queue: Stephen Adams <sra@google.com>
This commit is contained in:
parent
c25308d003
commit
aec79c2b3c
|
@ -561,10 +561,33 @@ abstract class JavaScriptNodeSourceInformation {
|
|||
}
|
||||
|
||||
abstract class Node {
|
||||
JavaScriptNodeSourceInformation? get sourceInformation => _sourceInformation;
|
||||
|
||||
/// Field for storing source information and other annotations.
|
||||
///
|
||||
/// Source information is common but not universal, so missing source
|
||||
/// information is represented by `null`. Annotations are uncommon. As a
|
||||
/// space-saving measure we pack both kinds of information into one field by
|
||||
/// storing the user's JavaScriptNodeSourceInformation object directly in the
|
||||
/// field if there are no annotations. If there are annotations, the field is
|
||||
/// 'expanded' by having it reference an internal
|
||||
/// [_SourceInformationAndAnnotations] object which has two fields.
|
||||
JavaScriptNodeSourceInformation? _sourceInformation;
|
||||
|
||||
/// Returns the source information associated with this node.
|
||||
JavaScriptNodeSourceInformation? get sourceInformation {
|
||||
final source = _sourceInformation;
|
||||
return source is _SourceInformationAndAnnotations
|
||||
? source._sourceInformation
|
||||
: source;
|
||||
}
|
||||
|
||||
/// Returns a list of annotations attached to this node. See [withAnnotation].
|
||||
List<Object> get annotations {
|
||||
final source = _sourceInformation;
|
||||
return source is _SourceInformationAndAnnotations
|
||||
? source._annotations
|
||||
: const [];
|
||||
}
|
||||
|
||||
T accept<T>(NodeVisitor<T> visitor);
|
||||
void visitChildren<T>(NodeVisitor<T> visitor);
|
||||
|
||||
|
@ -573,22 +596,49 @@ abstract class Node {
|
|||
|
||||
/// Shallow clone of node.
|
||||
///
|
||||
/// Does not clone positions since the only use of this private method is
|
||||
/// create a copy with a new position.
|
||||
/// Does not clone [_sourceInformation] since the only use of this private
|
||||
/// method is create a copy with a new source information or annotations.
|
||||
Node _clone();
|
||||
|
||||
/// Returns a node equivalent to [this], but with new source position and end
|
||||
/// source position.
|
||||
/// Returns a node equivalent to [this], but with new source position.
|
||||
Node withSourceInformation(
|
||||
JavaScriptNodeSourceInformation? sourceInformation) {
|
||||
if (sourceInformation == _sourceInformation) {
|
||||
return this;
|
||||
}
|
||||
Node clone = _clone();
|
||||
// TODO(sra): Should existing data be 'sticky' if we try to overwrite with
|
||||
JavaScriptNodeSourceInformation? newSourceInformation) {
|
||||
if (!_shouldReplaceSourceInformation(newSourceInformation)) return this;
|
||||
return _clone()
|
||||
.._sourceInformation =
|
||||
_replacementSourceInformation(newSourceInformation);
|
||||
}
|
||||
|
||||
bool _shouldReplaceSourceInformation(
|
||||
JavaScriptNodeSourceInformation? newSourceInformation) {
|
||||
// TODO(sra): Should existing data be 'sticky' if we try to update with
|
||||
// `null`?
|
||||
clone._sourceInformation = sourceInformation;
|
||||
return clone;
|
||||
return newSourceInformation != sourceInformation;
|
||||
}
|
||||
|
||||
JavaScriptNodeSourceInformation? _replacementSourceInformation(
|
||||
JavaScriptNodeSourceInformation? newSourceInformation) {
|
||||
final source = _sourceInformation;
|
||||
return source is _SourceInformationAndAnnotations
|
||||
? _SourceInformationAndAnnotations(
|
||||
newSourceInformation, source._annotations)
|
||||
: newSourceInformation;
|
||||
}
|
||||
|
||||
/// Returns a node equivalent to [this] but with an additional annotation.
|
||||
///
|
||||
/// Annotations are data attached to a Node. What exactly is stored as an
|
||||
/// annotation is determined by the user of the js_ast library. The
|
||||
/// annotations do not affect how the AST prints, but can be inspected either
|
||||
/// while walking the AST, either directly in a visitor or indirectly, e.g. by
|
||||
/// the enter/exit hooks in the printer.
|
||||
Node withAnnotation(Object newAnnotation) {
|
||||
return _clone().._sourceInformation = _appendedAnnotation(newAnnotation);
|
||||
}
|
||||
|
||||
_SourceInformationAndAnnotations _appendedAnnotation(Object newAnnotation) {
|
||||
return _SourceInformationAndAnnotations(
|
||||
sourceInformation, List.unmodifiable([...annotations, newAnnotation]));
|
||||
}
|
||||
|
||||
bool get isCommaOperator => false;
|
||||
|
@ -612,6 +662,14 @@ abstract class Node {
|
|||
}
|
||||
}
|
||||
|
||||
class _SourceInformationAndAnnotations
|
||||
implements JavaScriptNodeSourceInformation {
|
||||
final JavaScriptNodeSourceInformation? _sourceInformation;
|
||||
final List<Object> _annotations;
|
||||
_SourceInformationAndAnnotations(this._sourceInformation, this._annotations)
|
||||
: assert(_sourceInformation is! _SourceInformationAndAnnotations);
|
||||
}
|
||||
|
||||
class Program extends Node {
|
||||
final List<Statement> body;
|
||||
Program(this.body);
|
||||
|
@ -649,9 +707,11 @@ abstract class Statement extends Node {
|
|||
// Override for refined return type.
|
||||
@override
|
||||
Statement withSourceInformation(
|
||||
JavaScriptNodeSourceInformation? sourceInformation) {
|
||||
if (sourceInformation == _sourceInformation) return this;
|
||||
return _clone().._sourceInformation = sourceInformation;
|
||||
JavaScriptNodeSourceInformation? newSourceInformation) {
|
||||
if (!_shouldReplaceSourceInformation(newSourceInformation)) return this;
|
||||
return _clone()
|
||||
.._sourceInformation =
|
||||
_replacementSourceInformation(newSourceInformation);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -1312,9 +1372,11 @@ abstract class Expression extends Node {
|
|||
// Override for refined return type.
|
||||
@override
|
||||
Expression withSourceInformation(
|
||||
JavaScriptNodeSourceInformation? sourceInformation) {
|
||||
if (sourceInformation == _sourceInformation) return this;
|
||||
return _clone().._sourceInformation = sourceInformation;
|
||||
JavaScriptNodeSourceInformation? newSourceInformation) {
|
||||
if (!_shouldReplaceSourceInformation(newSourceInformation)) return this;
|
||||
return _clone()
|
||||
.._sourceInformation =
|
||||
_replacementSourceInformation(newSourceInformation);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
135
pkg/js_ast/test/annotations_test.dart
Normal file
135
pkg/js_ast/test/annotations_test.dart
Normal file
|
@ -0,0 +1,135 @@
|
|||
// Copyright (c) 2023, 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.
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
import 'package:js_ast/js_ast.dart';
|
||||
|
||||
class Source implements JavaScriptNodeSourceInformation {
|
||||
final Object tag;
|
||||
Source(this.tag);
|
||||
|
||||
@override
|
||||
String toString() => 'Source($tag)';
|
||||
}
|
||||
|
||||
void check(Node node, expectedSource, expectedAnnotations) {
|
||||
Expect.equals('$expectedSource', '${node.sourceInformation}', 'source');
|
||||
Expect.equals('$expectedAnnotations', '${node.annotations}', 'annotations');
|
||||
}
|
||||
|
||||
void simpleTests(Node node) {
|
||||
check(node, null, []);
|
||||
|
||||
final s1 = node.withSourceInformation(Source(1));
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
|
||||
final a1 = node.withAnnotation(1);
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
check(a1, null, [1]);
|
||||
|
||||
final a2 = node.withAnnotation(2);
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
check(a1, null, [1]);
|
||||
check(a2, null, [2]);
|
||||
|
||||
final s1a3 = s1.withAnnotation(3);
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
check(s1, Source(1), []);
|
||||
check(a2, null, [2]);
|
||||
check(s1a3, Source(1), [3]);
|
||||
|
||||
final s1a3a4 = s1a3.withAnnotation(4);
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
check(s1, Source(1), []);
|
||||
check(a2, null, [2]);
|
||||
check(s1a3, Source(1), [3]);
|
||||
check(s1a3a4, Source(1), [3, 4]);
|
||||
|
||||
final a2s5 = a2.withSourceInformation(Source(5));
|
||||
check(node, null, []);
|
||||
check(s1, Source(1), []);
|
||||
check(s1, Source(1), []);
|
||||
check(a2, null, [2]);
|
||||
check(s1a3, Source(1), [3]);
|
||||
check(s1a3a4, Source(1), [3, 4]);
|
||||
check(a2s5, Source(5), [2]);
|
||||
}
|
||||
|
||||
bool debugging = false;
|
||||
|
||||
/// Explore all combinations of withSourceInformation and withAnnotation.
|
||||
void testGraph(Node root) {
|
||||
// At each node in the graph, all the previous checks are re-run to ensure
|
||||
// that source information or annotations do not change.
|
||||
List<void Function(String state)> tests = [];
|
||||
|
||||
void explore(
|
||||
String state,
|
||||
Node node,
|
||||
int sourceDepth,
|
||||
int annotationDepth,
|
||||
JavaScriptNodeSourceInformation? expectedSource,
|
||||
List<Object> expectedAnnotations) {
|
||||
void newCheck(String currentState) {
|
||||
if (debugging) {
|
||||
print('In state $currentState check $state:'
|
||||
' source: $expectedSource, annotations: $expectedAnnotations');
|
||||
}
|
||||
Expect.equals(
|
||||
'$expectedSource',
|
||||
'${node.sourceInformation}',
|
||||
' at state $currentState for node at state $state:'
|
||||
' ${node.debugPrint()}');
|
||||
Expect.equals(
|
||||
'$expectedAnnotations',
|
||||
'${node.annotations}',
|
||||
' at state $currentState for node at state $state:'
|
||||
' ${node.debugPrint()}');
|
||||
}
|
||||
|
||||
tests.add(newCheck);
|
||||
|
||||
for (final test in tests) {
|
||||
test(state);
|
||||
}
|
||||
|
||||
if (sourceDepth < 3) {
|
||||
final newSourceDepth = sourceDepth + 1;
|
||||
final newSource = Source(newSourceDepth);
|
||||
final newState = '$state-s$newSourceDepth';
|
||||
final newNode = node.withSourceInformation(newSource);
|
||||
|
||||
explore(newState, newNode, newSourceDepth, annotationDepth, newSource,
|
||||
expectedAnnotations);
|
||||
}
|
||||
if (annotationDepth < 3) {
|
||||
final newAnnotationDepth = annotationDepth + 1;
|
||||
final newAnnotation = 'a:$newAnnotationDepth';
|
||||
final newAnnotations = [...expectedAnnotations, newAnnotation];
|
||||
final newState = '$state-a$newAnnotationDepth';
|
||||
final newNode = node.withAnnotation(newAnnotation);
|
||||
|
||||
explore(newState, newNode, sourceDepth, newAnnotationDepth,
|
||||
expectedSource, newAnnotations);
|
||||
}
|
||||
}
|
||||
|
||||
explore('root', root, 0, 0, null, []);
|
||||
}
|
||||
|
||||
void main() {
|
||||
simpleTests(js('x + 1'));
|
||||
simpleTests(js.statement('f()'));
|
||||
|
||||
testGraph(js('1'));
|
||||
testGraph(js('x + 1'));
|
||||
testGraph(js('f()'));
|
||||
testGraph(js.statement('f()'));
|
||||
testGraph(js.statement('break'));
|
||||
}
|
Loading…
Reference in a new issue