[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:
Stephen Adams 2023-02-07 21:34:16 +00:00 committed by Commit Queue
parent c25308d003
commit aec79c2b3c
2 changed files with 217 additions and 20 deletions

View file

@ -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

View 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'));
}