diff --git a/CHANGELOG.md b/CHANGELOG.md index bc64abd21..4bf43c72d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # OverReact Changelog +## Unreleased +- Remove dependency on `transformer_utils` + ## 5.5.0 - [#989] Optimize generated code to decrease dart2js compile size, saving ~577 bytes per component (when using `-03 --csp --minify`) - [#992] Fix compilation errors for legacy boilerplate defined in libraries with a Dart language version of >=3.0 diff --git a/lib/src/builder/codegen/accessors_generator.dart b/lib/src/builder/codegen/accessors_generator.dart index 2951cc3f7..d5e6696b8 100644 --- a/lib/src/builder/codegen/accessors_generator.dart +++ b/lib/src/builder/codegen/accessors_generator.dart @@ -14,7 +14,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:over_react/src/component_declaration/annotations.dart' as annotations; -import 'package:transformer_utils/transformer_utils.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; import '../parsing.dart'; import '../util.dart'; diff --git a/lib/src/builder/codegen/component_factory_generator.dart b/lib/src/builder/codegen/component_factory_generator.dart index 0fd9223af..1fc05dfbb 100644 --- a/lib/src/builder/codegen/component_factory_generator.dart +++ b/lib/src/builder/codegen/component_factory_generator.dart @@ -14,7 +14,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:over_react/src/component_declaration/annotations.dart' as annotations; -import 'package:transformer_utils/transformer_utils.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; import '../parsing.dart'; import '../util.dart'; diff --git a/lib/src/builder/codegen/typed_map_impl_generator.dart b/lib/src/builder/codegen/typed_map_impl_generator.dart index 887c75e67..728c07eb0 100644 --- a/lib/src/builder/codegen/typed_map_impl_generator.dart +++ b/lib/src/builder/codegen/typed_map_impl_generator.dart @@ -13,7 +13,7 @@ // limitations under the License. import 'package:analyzer/dart/ast/ast.dart'; -import 'package:transformer_utils/transformer_utils.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; import '../parsing.dart'; import '../util.dart'; diff --git a/lib/src/builder/parsing/ast_util.dart b/lib/src/builder/parsing/ast_util.dart index 2294bb273..5fc5f8e11 100644 --- a/lib/src/builder/parsing/ast_util.dart +++ b/lib/src/builder/parsing/ast_util.dart @@ -18,7 +18,7 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:over_react/src/builder/codegen/names.dart'; import 'package:source_span/source_span.dart'; -import 'package:transformer_utils/transformer_utils.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; import 'ast_util/classish.dart'; import 'util.dart'; diff --git a/lib/src/builder/parsing/meta.dart b/lib/src/builder/parsing/meta.dart index f4ea2c527..9f9b44345 100644 --- a/lib/src/builder/parsing/meta.dart +++ b/lib/src/builder/parsing/meta.dart @@ -17,7 +17,7 @@ import 'dart:mirrors' as mirrors; import 'package:analyzer/dart/ast/ast.dart'; import 'package:build/build.dart' show log; import 'package:collection/collection.dart' show IterableExtension; -import 'package:transformer_utils/transformer_utils.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; import 'ast_util.dart'; diff --git a/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart new file mode 100644 index 000000000..788c1ed99 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart @@ -0,0 +1,251 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +import 'dart:mirrors' as mirrors; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:collection/collection.dart' show IterableExtension; + +/// Returns a copy of a class [member] declaration with [body] as a new +/// implementation. +/// +/// Currently only supports: +/// * [FieldDeclaration] (single variable only) +/// * [MethodDeclaration] (getter, setter, and methods) +String copyClassMember(ClassMember? member, String body) { + if (member is FieldDeclaration) return _copyFieldDeclaration(member, body); + if (member is MethodDeclaration) { + if (member.isGetter) return _copyGetterDeclaration(member, body); + if (member.isSetter) return _copySetterDeclaration(member, body); + return _copyMethodDeclaration(member, body); + } + throw UnsupportedError('Unsupported class member type: ${member.runtimeType}. ' + 'Only FieldDeclaration and MethodDeclaration are supported.'); +} + +/// Finds and returns all declarations within a compilation [unit] that are +/// annotated with the given [annotation] class. +/// +/// If this is being leveraged within a transformer, you can associate the +/// returned [DeclarationWithMeta] instance with the asset in which it is +/// located by passing in an [assetId]. +Iterable getDeclarationsAnnotatedBy(CompilationUnit unit, Type annotation) { + var annotationName = _getReflectedName(annotation); + return unit.declarations.where((member) { + return member.metadata.any((meta) => meta.name.name == annotationName); + }); +} + +/// Returns the value of the specified [expression] AST node if it represents a literal. +/// +/// For non-literal nodes, the return value will be the result of calling [onUnsupportedExpression] with [expression]. +/// +/// If [onUnsupportedExpression] isn't specified, an [UnsupportedError] will be thrown. +/// +/// Currently only supports: +/// * [StringLiteral] +/// * [BooleanLiteral] +/// * [IntegerLiteral] +/// * [NullLiteral] +dynamic getValue(Expression expression, + {dynamic Function(Expression expression)? onUnsupportedExpression}) { + if (expression is StringLiteral) { + var value = expression.stringValue; + if (value != null) { + return value; + } + } else if (expression is BooleanLiteral) { + return expression.value; + } else if (expression is IntegerLiteral) { + return expression.value; + } else if (expression is NullLiteral) { + return null; + } + + if (onUnsupportedExpression != null) { + return onUnsupportedExpression(expression); + } + + throw UnsupportedError('Unsupported expression: $expression. ' + 'Must be a uninterpolated string, boolean, integer, or null literal.'); +} + +/// Returns the first annotation AST node on [member] of type [annotationType], +/// or null if no matching annotations are found. +Annotation? getMatchingAnnotation(AnnotatedNode member, Type annotationType) { + // Be sure to use `originalDeclaration` so that generic parameters work. + final classMirror = + mirrors.reflectClass(annotationType).originalDeclaration as mirrors.ClassMirror; + String className = mirrors.MirrorSystem.getName(classMirror.simpleName); + + // Find the annotation that matches [type]'s name. + return member.metadata.firstWhereOrNull((annotation) { + return _getClassName(annotation) == className; + }); +} + +/// Uses reflection to instantiate and returns the first annotation on [member] of type +/// [annotationType], or null if no matching annotations are found. +/// +/// Annotation constructors are currently limited to the values supported by [getValue]. +/// +/// Naively assumes that the name of the [annotationType] class is canonical. +dynamic instantiateAnnotation(AnnotatedNode member, Type annotationType, + {dynamic Function(Expression argument)? onUnsupportedArgument}) { + final matchingAnnotation = getMatchingAnnotation(member, annotationType); + + // If no annotation is found, return null. + if (matchingAnnotation == null) { + return null; + } + + final matchingAnnotationArgs = matchingAnnotation.arguments; + if (matchingAnnotationArgs == null) { + throw Exception('Annotation not invocation of constructor: `$matchingAnnotation`. ' + 'This is likely due to invalid usage of the annotation class, but could' + 'also be a name conflict with the specified type `$annotationType`'); + } + + // Get the parameters from the annotation's AST. + Map namedParameters = {}; + List positionalParameters = []; + + matchingAnnotationArgs.arguments.forEach((argument) { + var onUnsupportedExpression = + onUnsupportedArgument == null ? null : (_) => onUnsupportedArgument(argument); + + if (argument is NamedExpression) { + var name = argument.name.label.name; + var value = getValue(argument.expression, onUnsupportedExpression: onUnsupportedExpression); + + namedParameters[Symbol(name)] = value; + } else { + var value = getValue(argument, onUnsupportedExpression: onUnsupportedExpression); + + positionalParameters.add(value); + } + }); + + // Instantiate and return an instance of the annotation using reflection. + String constructorName = _getConstructorName(matchingAnnotation) ?? ''; + + // Be sure to use `originalDeclaration` so that generic parameters work. + final classMirror = + mirrors.reflectClass(annotationType).originalDeclaration as mirrors.ClassMirror; + + try { + var instanceMirror = + classMirror.newInstance(Symbol(constructorName), positionalParameters, namedParameters); + return instanceMirror.reflectee; + } catch (e, stacktrace) { + throw Exception('Unable to instantiate annotation: $matchingAnnotation. This is ' + 'likely due to improper usage, or a naming conflict with ' + 'annotationType $annotationType. Original error: $e. Stacktrace: $stacktrace'); + } +} + +String _copyFieldDeclaration(FieldDeclaration decl, String initializer) { + var result = ''; + if (decl.fields.type != null) { + result = '${decl.fields.type}'; + } else if (decl.staticKeyword == null) { + result = 'var'; + } + if (decl.staticKeyword != null) { + result = '${decl.staticKeyword} $result'; + } + result = '$result ${decl.fields.variables.first.name.lexeme}'; + if (initializer.isNotEmpty) { + result = '$result = $initializer;'; + } else { + result = '$result;'; + } + return result; +} + +String _copyGetterDeclaration(MethodDeclaration decl, String body) { + var result = ''; + if (decl.returnType != null) { + result = '${decl.returnType} get'; + } else { + result = 'get'; + } + if (decl.isStatic) { + result = 'static $result'; + } + + result = '$result ${decl.name.lexeme}'; + if (decl.body.keyword != null) { + result = '$result ${decl.body.keyword}${decl.body.star ?? ''}'; + } + result = '$result {\n$body\n}'; + return result; +} + +String _copySetterDeclaration(MethodDeclaration decl, String body) { + var result = 'void set'; + if (decl.isStatic) { + result = 'static $result'; + } + result = '$result ${decl.name.lexeme}${decl.parameters} {\n$body\n }'; + return result; +} + +String _copyMethodDeclaration(MethodDeclaration decl, String body) { + var result = '${decl.name.lexeme}'; + if (decl.returnType != null) { + result = '${decl.returnType} $result'; + } + if (decl.isStatic) { + result = 'static $result'; + } + result = '$result${decl.parameters}'; + if (decl.body.keyword != null) { + result = '$result ${decl.body.keyword}${decl.body.star ?? ''}'; + } + result = '$result {\n$body\n }'; + return result; +} + +/// Returns the name of the class being instantiated for [annotation], or null +/// if the annotation is not the invocation of a constructor. +/// +/// Workaround for a Dart analyzer issue where the constructor name is included +/// in [annotation.name]. +String _getClassName(Annotation annotation) => annotation.name.name.split('.').first; + +/// Returns the name of the constructor being instantiated for [annotation], or +/// null if the annotation is not the invocation of a named constructor. +/// +/// Workaround for a Dart analyzer issue where the constructor name is included +/// in [annotation.name]. +String? _getConstructorName(Annotation annotation) { + var constructorName = annotation.constructorName?.name; + if (constructorName == null) { + var periodIndex = annotation.name.name.indexOf('.'); + if (periodIndex != -1) { + constructorName = annotation.name.name.substring(periodIndex + 1); + } + } + + return constructorName; +} + +/// Get the name of a [type] via reflection. +String _getReflectedName(Type type) => + mirrors.MirrorSystem.getName(mirrors.reflectType(type).simpleName); diff --git a/lib/src/builder/vendor/transformer_utils/src/barback_utils.dart b/lib/src/builder/vendor/transformer_utils/src/barback_utils.dart new file mode 100644 index 000000000..86012a7e2 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/src/barback_utils.dart @@ -0,0 +1,43 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:build/build.dart'; +import 'package:path/path.dart' as path; +import 'package:source_span/source_span.dart'; + +/// Converts [id] to a "package:" URI. +/// +/// This will return a schemeless URI if [id] doesn't represent a library in +/// `lib/`. +Uri assetIdToPackageUri(AssetId id) { + if (!id.path.startsWith('lib/')) return Uri(path: id.path); + return Uri(scheme: 'package', path: path.url.join(id.package, id.path.replaceFirst('lib/', ''))); +} + +/// Returns a [SourceSpan] spanning from the beginning to the end of the given +/// [node]. The preceding comment and metadata will be excluded if +/// [skipCommentAndMetadata] is true. +SourceSpan getSpanForNode(SourceFile sourceFile, AstNode node, + {bool skipCommentAndMetadata = true}) { + if (skipCommentAndMetadata && node is AnnotatedNode) { + return sourceFile.span(node.firstTokenAfterCommentAndMetadata.offset, node.end); + } + + return sourceFile.span(node.offset, node.end); +} diff --git a/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart b/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart new file mode 100644 index 000000000..54c806645 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/src/node_with_meta.dart @@ -0,0 +1,68 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:build/build.dart'; + +import './analyzer_helpers.dart'; + +/// Utility class that allows for easy access to an annotated node's +/// instantiated annotation. +class NodeWithMeta { + /// The optionally-annotated node. + final TNode node; + + /// The optional asset ID associated with this node. + final AssetId? assetId; + + /// The node of the [TMeta] annotation, if it exists. + final Annotation? metaNode; + + /// A reflectively-instantiated version of [metaNode], if it exists. + TMeta? _meta; + + /// The arguments passed to the metadata that are not supported by [getValue], + /// (or by special handling in subclasses) and therefore not represented in the instantiation of [meta]. + final List unsupportedArguments = []; + + /// Construct a [NodeWithMeta] instance from an [AnnotatedNode]. + /// The original node will be available via [node]. + /// The instantiated annotation of type `TMeta` will be available via [meta]. + NodeWithMeta(this.node, {this.assetId}) : this.metaNode = getMatchingAnnotation(node, TMeta) { + this._meta = instantiateAnnotation(node, TMeta, onUnsupportedArgument: unsupportedArguments.add) + as TMeta?; + } + + /// Whether this node's metadata has arguments that could not be initialized using [getValue] + /// (or by special handling in subclasses), and therefore cannot represented in the instantiation of [meta]. + bool get isIncomplete => unsupportedArguments.isNotEmpty; + + /// A reflectively-instantiated version of [metaNode], if it exists. + /// + /// Throws a [StateError] if this node's metadata is incomplete. + TMeta? get meta { + if (isIncomplete) { + throw StateError('Metadata is incomplete; unsupported arguments $unsupportedArguments. ' + 'Use `potentiallyIncompleteMeta` instead.'); + } + return _meta; + } + + /// A reflectively-instantiated version of [metaNode], if it exists. + TMeta? get potentiallyIncompleteMeta => _meta; +} diff --git a/lib/src/builder/vendor/transformer_utils/src/text_util.dart b/lib/src/builder/vendor/transformer_utils/src/text_util.dart new file mode 100644 index 000000000..a6cbe2b69 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/src/text_util.dart @@ -0,0 +1,150 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +/// Returns [content] escaped and optionally quoted for use as a string literal +/// in Dart source code. +/// +/// If [content] is null, a string version of the the `null` literal will be +/// returned instead. +String stringLiteral(String? content, {bool quote = true, bool useSingleQuote = true}) { + // Adapted from dart.convert library's JSON encoder: + // https://github.com/dart-lang/sdk/blob/1.12.0/sdk/lib/convert/json.dart#L565 + // + // + // Copyright 2012, the Dart project authors. All rights reserved. + // Redistribution and use in source and binary forms, with or without + // modification, are permitted provided that the following conditions are + // met: + // * Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimer. + // * Redistributions in binary form must reproduce the above + // copyright notice, this list of conditions and the following + // disclaimer in the documentation and/or other materials provided + // with the distribution. + // * Neither the name of Google Inc. nor the names of its + // contributors may be used to endorse or promote products derived + // from this software without specific prior written permission. + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + if (content == null) { + return 'null'; + } + + const int BACKSPACE = 0x08; + const int TAB = 0x09; + const int NEWLINE = 0x0a; + const int CARRIAGE_RETURN = 0x0d; + const int FORM_FEED = 0x0c; + const int DOLLAR_SIGN = 0x24; + const int SINGLE_QUOTE = 0x27; + const int DOUBLE_QUOTE = 0x22; + const int CHAR_0 = 0x30; + const int BACKSLASH = 0x5c; + const int CHAR_b = 0x62; + const int CHAR_f = 0x66; + const int CHAR_n = 0x6e; + const int CHAR_r = 0x72; + const int CHAR_t = 0x74; + const int CHAR_u = 0x75; + + int quoteChar = useSingleQuote ? SINGLE_QUOTE : DOUBLE_QUOTE; + + var buffer = StringBuffer(); + + void writeStringSlice(String string, int start, int end) { + buffer.write(string.substring(start, end)); + } + + void writeCharCode(int charCode) { + buffer.writeCharCode(charCode); + } + + void writeString(String string) { + buffer.write(string); + } + + // ('0' + x) or ('a' + x - 10) + int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; + + if (quote) { + writeCharCode(quoteChar); + } + + int offset = 0; + final int length = content.length; + for (int i = 0; i < length; i++) { + int charCode = content.codeUnitAt(i); + if (charCode > BACKSLASH) continue; + if (charCode < 32) { + if (i > offset) writeStringSlice(content, offset, i); + offset = i + 1; + writeCharCode(BACKSLASH); + switch (charCode) { + case BACKSPACE: + writeCharCode(CHAR_b); + break; + case TAB: + writeCharCode(CHAR_t); + break; + case NEWLINE: + writeCharCode(CHAR_n); + break; + case FORM_FEED: + writeCharCode(CHAR_f); + break; + case CARRIAGE_RETURN: + writeCharCode(CHAR_r); + break; + default: + writeCharCode(CHAR_u); + writeCharCode(CHAR_0); + writeCharCode(CHAR_0); + writeCharCode(hexDigit((charCode >> 4) & 0xf)); + writeCharCode(hexDigit(charCode & 0xf)); + break; + } + } else if (charCode == quoteChar || charCode == BACKSLASH || charCode == DOLLAR_SIGN) { + if (i > offset) writeStringSlice(content, offset, i); + offset = i + 1; + writeCharCode(BACKSLASH); + writeCharCode(charCode); + } + } + + if (offset == 0) { + writeString(content); + } else if (offset < length) { + writeStringSlice(content, offset, length); + } + + if (quote) { + writeCharCode(quoteChar); + } + + return buffer.toString(); +} diff --git a/lib/src/builder/vendor/transformer_utils/src/transformed_source_file.dart b/lib/src/builder/vendor/transformer_utils/src/transformed_source_file.dart new file mode 100644 index 000000000..e8ed2d510 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/src/transformed_source_file.dart @@ -0,0 +1,162 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +import 'dart:convert'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:source_span/source_span.dart'; + +/// A record used internally by [TransformedSourceFile] that represents the +/// replacement of a [SourceSpan] with a string of text. +class _Replacement { + final SourceSpan span; + final String newText; + + _Replacement(this.span, this.newText); + + @override + String toString() => 'Replacement (span: $span, newText: $newText)'; +} + +/// A utility that allows for modification of a [SourceFile] via a series of +/// replacements, insertions, and removals. +class TransformedSourceFile { + final List<_Replacement> _replacements = []; + + final SourceFile sourceFile; + + TransformedSourceFile(this.sourceFile); + + bool get isModified => _replacements.isNotEmpty; + + void replace(SourceSpan span, String text) { + _replacements.add(_Replacement(span, text)); + } + + void insert(SourceLocation location, String text) { + _replacements.add(_Replacement(location.pointSpan(), text)); + } + + void remove(SourceSpan span, {bool preserveNewlines = false}) { + String replacement; + if (preserveNewlines) { + replacement = '\n' * '\n'.allMatches(span.text).length; + } else { + replacement = ''; + } + + _replacements.add(_Replacement(span, replacement)); + } + + void iterateReplacements( + {Function(String string)? onUnmodified, + Function(String string)? onRemoval, + Function(String string)? onAddition}) { + _replacements.sort((r1, r2) => r1.span.compareTo(r2.span)); + + var lastEdge = 0; + for (_Replacement replacement in _replacements) { + if (replacement.span.start.offset < lastEdge) { + throw Exception('Overlapping replacement $replacement in replacements $_replacements.'); + } + + var unmodifiedText = sourceFile.getText(lastEdge, replacement.span.start.offset); + var removalText = replacement.span.text; + var additionText = replacement.newText; + + if (onUnmodified != null && unmodifiedText.isNotEmpty) { + onUnmodified(unmodifiedText); + } + if (onRemoval != null && removalText.isNotEmpty) { + onRemoval(removalText); + } + if (onAddition != null && additionText.isNotEmpty) { + onAddition(additionText); + } + + lastEdge = replacement.span.end.offset; + } + + var unmodifiedText = sourceFile.getText(lastEdge); + if (onUnmodified != null) { + onUnmodified(unmodifiedText); + } + } + + String getTransformedText() { + StringBuffer transformedSource = StringBuffer(); + + iterateReplacements(onUnmodified: transformedSource.write, onAddition: transformedSource.write); + + return transformedSource.toString(); + } + + String getHtmlDiff() { + const HtmlEscape elementEscaper = HtmlEscape(HtmlEscapeMode.element); + const HtmlEscape attrEscaper = HtmlEscape(HtmlEscapeMode.attribute); + + StringBuffer diff = StringBuffer(); + + void writeDiff(String source, String className) { + diff.write(''); + diff.write(elementEscaper.convert(source)); + diff.write(''); + } + + iterateReplacements( + onUnmodified: (source) => writeDiff(source, 'diff-unmodified'), + onRemoval: (source) => writeDiff(source, 'diff-removal'), + onAddition: (source) => writeDiff(source, 'diff-addition')); + + return ''' + + + + Transformer Diff - ${attrEscaper.convert(sourceFile.url!.path)} + + + + +
$diff
+
+ + + '''; + } +} + +SourceSpan getSpan(SourceFile sourceFile, AstNode node, {bool skipCommentAndMetadata = true}) { + if (skipCommentAndMetadata && node is AnnotatedNode) { + return sourceFile.span(node.firstTokenAfterCommentAndMetadata.offset, node.end); + } + + return sourceFile.span(node.offset, node.end); +} diff --git a/lib/src/builder/vendor/transformer_utils/transformer_utils.dart b/lib/src/builder/vendor/transformer_utils/transformer_utils.dart new file mode 100644 index 000000000..3cac85dd7 --- /dev/null +++ b/lib/src/builder/vendor/transformer_utils/transformer_utils.dart @@ -0,0 +1,24 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/lib/ + +library; + +export './src/analyzer_helpers.dart' + show copyClassMember, getDeclarationsAnnotatedBy, instantiateAnnotation; +export './src/barback_utils.dart' show assetIdToPackageUri, getSpanForNode; +export './src/node_with_meta.dart' show NodeWithMeta; +export './src/text_util.dart' show stringLiteral; +export './src/transformed_source_file.dart' show TransformedSourceFile, getSpan; diff --git a/pubspec.yaml b/pubspec.yaml index 83550b380..95b1ed28d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,7 +19,6 @@ dependencies: react: ^7.3.1 redux: ^5.0.0 source_span: ^1.4.1 - transformer_utils: ^0.2.6 w_common: ^3.0.0 w_flux: ^3.0.0 platform_detect: ^2.0.0 diff --git a/test/vm_tests/builder/vendor/transformer_utils/test_utils.dart b/test/vm_tests/builder/vendor/transformer_utils/test_utils.dart new file mode 100644 index 000000000..b8ef2b0b8 --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/test_utils.dart @@ -0,0 +1,69 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +library; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:test/test.dart'; + +class TestAnnotation { + final dynamic positional; + final dynamic named; + final dynamic namedConstructorOnly; + const TestAnnotation(this.positional, {this.named}) + : namedConstructorOnly = null; + const TestAnnotation.namedConstructor({this.namedConstructorOnly}) + : positional = null, + named = null; +} + +ConstructorDeclaration? getConstructor(ClassDeclaration classDecl, + {String? name}) { + for (var member in classDecl.members) { + if (member is ConstructorDeclaration && member.name?.lexeme == name) { + return member; + } + } + return null; +} + +FieldDeclaration? getFieldByName(ClassDeclaration classDecl, String name) { + for (var member in classDecl.members) { + if (member is FieldDeclaration && + member.fields.variables.first.name.lexeme == name) { + return member; + } + } + return null; +} + +MethodDeclaration? getMethodByName(ClassDeclaration classDecl, String name) { + for (var member in classDecl.members) { + if (member is MethodDeclaration && member.name.lexeme == name) { + return member; + } + } + return null; +} + +T parseAndGetSingleMember(String source) { + var compilationUnit = + parseString(content: source, throwIfDiagnostics: false).unit; + return compilationUnit.declarations.single as T; +} + +Matcher hasToStringValue(dynamic value) => isA().having((e) => e.toString(), 'toString', value); diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart new file mode 100644 index 000000000..25c35e875 --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/analyzer_helpers_test.dart @@ -0,0 +1,386 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +@TestOn('vm') +library; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/src/analyzer_helpers.dart'; +import 'package:test/test.dart'; + + +import '../test_utils.dart'; + +const String classDef = ''' +class TestClass { + TestClass(); + + var field = 'field'; + String typedField = 'typedField'; + static String staticField = 'typedField'; + + get untypedGetter => null; + String get typedGetter => null; + static String get staticGetter => null; + Future get asyncGetter async => null; + + static void set staticSetter(v) { } + void set setter(v) { } + void set typedSetter(String v) { } + + untypedMethod() { } + String typedMethod() { } + static String staticMethod() { } + Future asyncMethod() async { } + String methodWithArgs(String a, {int b}) { } +} +'''; + +void expectLines(String actual, String expected) { + var expectedLines = expected.split('\n'); + var actualLines = actual.split('\n'); + for (var i = 0; i < expectedLines.length; i++) { + expect(actualLines[i].trim(), expectedLines[i].trim()); + } +} + +main() { + group('copyClassMember()', () { + test('throws on unsupported class member', () { + var node = parseAndGetSingleMember(classDef); + var ctor = getConstructor(node); + expect(() { + copyClassMember(ctor, 'body'); + }, throwsUnsupportedError); + }); + + group('FieldDeclaration', () { + test('copies an untyped field', () { + var node = parseAndGetSingleMember(classDef); + var field = getFieldByName(node, 'field'); + var expected = 'var field = "copy";'; + expect(copyClassMember(field, '"copy"'), equals(expected)); + }); + + test('copies a typed field', () { + var node = parseAndGetSingleMember(classDef); + var field = getFieldByName(node, 'typedField'); + var expected = 'String typedField = "copy";'; + expect(copyClassMember(field, '"copy"'), equals(expected)); + }); + + test('copies a static field', () { + var node = parseAndGetSingleMember(classDef); + var field = getFieldByName(node, 'staticField'); + var expected = 'static String staticField = "copy";'; + expect(copyClassMember(field, '"copy"'), equals(expected)); + }); + + test('copies a field witout an initializer', () { + var node = parseAndGetSingleMember(classDef); + var field = getFieldByName(node, 'typedField'); + var expected = 'String typedField;'; + expect(copyClassMember(field, ''), equals(expected)); + }); + }); + + group('MethodDeclaration (Getter)', () { + test('copies untyped getter', () { + var node = parseAndGetSingleMember(classDef); + var getter = getMethodByName(node, 'untypedGetter'); + var expected = + ['get untypedGetter {', 'return "copy";', '}'].join('\n'); + expectLines(copyClassMember(getter, 'return "copy";'), expected); + }); + + test('copies typed getter', () { + var node = parseAndGetSingleMember(classDef); + var getter = getMethodByName(node, 'typedGetter'); + var expected = + ['String get typedGetter {', 'return "copy";', '}'].join('\n'); + expectLines(copyClassMember(getter, 'return "copy";'), expected); + }); + + test('copies static getter', () { + var node = parseAndGetSingleMember(classDef); + var getter = getMethodByName(node, 'staticGetter'); + var expected = [ + 'static String get staticGetter {', + 'return "copy";', + '}' + ].join('\n'); + expectLines(copyClassMember(getter, 'return "copy";'), expected); + }); + + test('copies async getter', () { + var node = parseAndGetSingleMember(classDef); + var getter = getMethodByName(node, 'asyncGetter'); + var expected = ['Future get asyncGetter async {', 'return "copy";', '}'] + .join('\n'); + expectLines(copyClassMember(getter, 'return "copy";'), expected); + }); + }); + + group('MethodDeclaration (Setter)', () { + test('copies setter', () { + var node = parseAndGetSingleMember(classDef); + var setter = getMethodByName(node, 'setter'); + var expected = ['void set setter(v) {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(setter, 'c = "copy";'), expected); + }); + + test('copies typed setter', () { + var node = parseAndGetSingleMember(classDef); + var setter = getMethodByName(node, 'typedSetter'); + var expected = + ['void set typedSetter(String v) {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(setter, 'c = "copy";'), expected); + }); + + test('copies static setter', () { + var node = parseAndGetSingleMember(classDef); + var setter = getMethodByName(node, 'staticSetter'); + var expected = ['static void set staticSetter(v) {', 'c = "copy";', '}'] + .join('\n'); + expectLines(copyClassMember(setter, 'c = "copy";'), expected); + }); + }); + + group('MethodDeclaration', () { + test('copies untyped method', () { + var node = parseAndGetSingleMember(classDef); + var method = getMethodByName(node, 'untypedMethod'); + var expected = ['untypedMethod() {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(method, 'c = "copy";'), expected); + }); + + test('copies typed method', () { + var node = parseAndGetSingleMember(classDef); + var method = getMethodByName(node, 'typedMethod'); + var expected = + ['String typedMethod() {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(method, 'c = "copy";'), expected); + }); + + test('copies static method', () { + var node = parseAndGetSingleMember(classDef); + var method = getMethodByName(node, 'staticMethod'); + var expected = + ['static String staticMethod() {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(method, 'c = "copy";'), expected); + }); + + test('copies async method', () async { + var node = parseAndGetSingleMember(classDef); + var method = getMethodByName(node, 'asyncMethod'); + var expected = + ['Future asyncMethod() async {', 'c = "copy";', '}'].join('\n'); + expectLines(copyClassMember(method, 'c = "copy";'), expected); + }); + + test('copies method with args', () async { + var node = parseAndGetSingleMember(classDef); + var method = getMethodByName(node, 'methodWithArgs'); + var expected = [ + 'String methodWithArgs(String a, {int b}) {', + 'c = "copy";', + '}' + ].join('\n'); + expectLines(copyClassMember(method, 'c = "copy";'), expected); + }); + }); + }); + + group('getDeclarationsAnnotatedBy()', () { + test('no matching declarations', () { + var result = parseString( + content: + ['var a;', '@OtherAnnotation()', 'var b;', 'var c;'].join('\n'), + throwIfDiagnostics: false); + var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); + expect(matches, isEmpty); + }); + + test('one matching declaration', () { + var result = parseString( + content: [ + '@TestAnnotation("test")', + 'var a;', + '@OtherAnnotation()', + 'var b;', + 'var c;' + ].join('\n'), + throwIfDiagnostics: false); + var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); + expect(matches.length, 1); + }); + + test('multiple matching declarations', () { + var result = parseString( + content: [ + '@TestAnnotation("test")', + 'var a;', + '@OtherAnnotation()', + 'var b;', + '@TestAnnotation("test")', + 'var c;' + ].join('\n'), + throwIfDiagnostics: false); + var matches = getDeclarationsAnnotatedBy(result.unit, TestAnnotation); + expect(matches.length, 2); + }); + }); + + group('instantiateAnnotation()', () { + group('instantiates an annotation with a parameter value specified as', () { + test('a string literal', () { + var node = parseAndGetSingleMember('@TestAnnotation("hello")\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, 'hello'); + }); + + test('a concatenated string literal', () { + var node = parseAndGetSingleMember('@TestAnnotation("he" "y")\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, 'hey'); + }); + + test('a boolean literal', () { + var node = parseAndGetSingleMember('@TestAnnotation(true)\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, true); + }); + + test('an integer literal', () { + var node = parseAndGetSingleMember('@TestAnnotation(1)\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, 1); + }); + + test('a null literal', () { + var node = parseAndGetSingleMember('@TestAnnotation(null)\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, null); + }); + }); + + group('throws when an annotation parameter value is unsupported:', () { + test('a constant expression', () { + var node = parseAndGetSingleMember('@TestAnnotation(const [])\nvar a;'); + expect(() => instantiateAnnotation(node, TestAnnotation), + throwsUnsupportedError); + }); + + test('an interpolated String', () { + var node = parseAndGetSingleMember('@TestAnnotation("\$v")\nvar a;'); + expect(() => instantiateAnnotation(node, TestAnnotation), + throwsUnsupportedError); + }); + + test('an identifier', () { + var node = + parseAndGetSingleMember('@TestAnnotation(identifier)\nvar a;'); + expect(() => instantiateAnnotation(node, TestAnnotation), + throwsUnsupportedError); + }); + + group('(except when `onUnsupportedArgument` is specified)', () { + test('positional parameter', () { + Expression? unsupportedArgument; + + final instance = instantiateAnnotation( + parseAndGetSingleMember('@TestAnnotation(const [])\nvar a;'), + TestAnnotation, onUnsupportedArgument: (Expression expression) { + unsupportedArgument = expression; + return 'value to be passed to constructor instead'; + }) as TestAnnotation; + + expect(unsupportedArgument, isA()); + expect(instance.positional, + equals('value to be passed to constructor instead'), + reason: + 'should have passed the return value of `onUnsupportedArgument` to the constructor'); + }); + + test('named parameter', () { + Expression? unsupportedArgument; + + final instance = instantiateAnnotation( + parseAndGetSingleMember( + '@TestAnnotation.namedConstructor(namedConstructorOnly: const [])\nvar a;'), + TestAnnotation, onUnsupportedArgument: (Expression expression) { + unsupportedArgument = expression; + return 'value to be passed to constructor instead'; + }) as TestAnnotation; + + expect(unsupportedArgument, isA()); + expect((unsupportedArgument! as NamedExpression).name.label.name, + equals('namedConstructorOnly')); + expect(instance.namedConstructorOnly, + equals('value to be passed to constructor instead'), + reason: + 'should have passed the return value of `onUnsupportedArgument` to the constructor'); + }); + }); + }); + + test('annotation with both named and positional parameters', () { + var node = + parseAndGetSingleMember('@TestAnnotation(1, named: 2)\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.positional, 1); + expect(instance.named, 2); + }); + + test('instantiates an annotation using a named constructor', () { + var node = parseAndGetSingleMember( + '@TestAnnotation.namedConstructor(namedConstructorOnly: true)\nvar a;'); + final instance = instantiateAnnotation(node, TestAnnotation) as TestAnnotation; + expect(instance.namedConstructorOnly, true); + }); + + test('throws if the annotation cannot be constructed', () { + var node = parseAndGetSingleMember( + '@TestAnnotation(1, 2, 3, 4, "way more parameters than were declared")\nvar a;'); + expect(() { + instantiateAnnotation(node, TestAnnotation); + }, throwsA(hasToStringValue(contains('Unable to instantiate annotation')))); + }); + + test('throws if the annotation is not used as a constructor', () { + var node = parseAndGetSingleMember('@TestAnnotation\nvar a;'); + expect(() { + instantiateAnnotation(node, TestAnnotation); + }, throwsA(hasToStringValue(contains('Annotation not invocation of constructor')))); + }); + + test('returns null when the member is not annotated', () { + var node = parseAndGetSingleMember('var a;'); + expect(instantiateAnnotation(node, TestAnnotation), isNull); + }); + + test('returns null when the member has only non-matching annotations', () { + var node = parseAndGetSingleMember('@NonexistantAnnotation\nvar a;'); + expect(instantiateAnnotation(node, TestAnnotation), isNull); + }); + + test('returns null when the member has no annotations', () { + var node = parseAndGetSingleMember('var a;'); + expect(instantiateAnnotation(node, TestAnnotation), isNull); + }); + }); +} diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/barback_utils_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/barback_utils_test.dart new file mode 100644 index 000000000..1abc6c75b --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/barback_utils_test.dart @@ -0,0 +1,84 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +@TestOn('vm') +library; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:build/build.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/src/barback_utils.dart'; +import 'package:source_span/source_span.dart'; +import 'package:test/test.dart'; + +const String sourceFileText = ''' +var plainVar = "plain"; + +/// Comment +@Annotation() +var varWithCommentAndMeta = "annotated"; +'''; + +void main() { + group('assetIdToPackageUri()', () { + test('returns original path for non-lib file', () { + var assetId = AssetId('transformer_utils', 'test/test_utils.dart'); + expect(assetIdToPackageUri(assetId), Uri.parse('test/test_utils.dart')); + }); + + test('returns path with "package" scheme for lib file', () { + var assetId = AssetId('transformer_utils', 'lib/transformer_utils.dart'); + expect(assetIdToPackageUri(assetId), + Uri.parse('package:transformer_utils/transformer_utils.dart')); + }); + }); + + group('getSpanForNode()', () { + test('should get node but skip comment and meta by default', () { + var sourceFile = SourceFile.fromString(sourceFileText); + var unit = + parseString(content: sourceFileText, throwIfDiagnostics: false).unit; + var annotatedNode = unit.childEntities.last as AstNode; + var span = getSpanForNode(sourceFile, annotatedNode); + expect(span.text, 'var varWithCommentAndMeta = "annotated";'); + }); + + test('should not skip comment and meta if skip is false', () { + var sourceFile = SourceFile.fromString(sourceFileText); + var unit = + parseString(content: sourceFileText, throwIfDiagnostics: false).unit; + var annotatedNode = unit.childEntities.last as AstNode; + var span = getSpanForNode(sourceFile, annotatedNode, + skipCommentAndMetadata: false); + expect( + span.text, + [ + '/// Comment', + '@Annotation()', + 'var varWithCommentAndMeta = "annotated";' + ].join('\n')); + }); + + test('should return the whole span if the node is not annotated', () { + var sourceFile = SourceFile.fromString(sourceFileText); + var unit = + parseString(content: sourceFileText, throwIfDiagnostics: false).unit; + var plainNode = unit.childEntities.first as AstNode; + var span = getSpanForNode(sourceFile, plainNode); + expect(span.text, 'var plainVar = "plain";'); + }); + }); +} diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart new file mode 100644 index 000000000..6d40676af --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/node_with_meta_test.dart @@ -0,0 +1,77 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +@TestOn('vm') +library; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:test/test.dart'; + +import 'package:over_react/src/builder/vendor/transformer_utils/transformer_utils.dart'; + +import '../test_utils.dart'; + +void main() { + group('NodeWithMeta', () { + test('instantiates and provides access to an annotation and node', () { + var member = parseAndGetSingleMember('@TestAnnotation("hello")\nvar a;'); + var nodeWithMeta = + NodeWithMeta( + member as TopLevelVariableDeclaration); + + expect(nodeWithMeta.node, same(member)); + expect(nodeWithMeta.metaNode, isNotNull); + expect(nodeWithMeta.metaNode!.name.name, 'TestAnnotation'); + expect(nodeWithMeta.meta, isNotNull); + expect(nodeWithMeta.meta!.positional, 'hello'); + }); + + test('partially instantiates an "incomplete" annotation', () { + var member = parseAndGetSingleMember( + '@TestAnnotation(someIdentifier, named: "hello")\nvar a;'); + var nodeWithMeta = + NodeWithMeta( + member as TopLevelVariableDeclaration); + + expect(nodeWithMeta.node, same(member)); + expect(nodeWithMeta.metaNode, isNotNull); + expect(nodeWithMeta.metaNode!.name.name, 'TestAnnotation'); + + expect(nodeWithMeta.isIncomplete, isTrue); + expect(nodeWithMeta.unsupportedArguments, hasLength(1)); + expect(() => nodeWithMeta.meta, throwsStateError); + + expect(nodeWithMeta.potentiallyIncompleteMeta, isNotNull, + reason: + 'should still have attempted to instantiate the incomplete annotation'); + expect(nodeWithMeta.potentiallyIncompleteMeta!.named, equals('hello'), + reason: 'should still have passed the supported argument'); + expect(nodeWithMeta.potentiallyIncompleteMeta!.positional, isNull, + reason: 'should have used null for unsupported argument'); + }); + + test('gracefully handles a node without an annotation', () { + var member = parseAndGetSingleMember('var a;'); + var nodeWithMeta = + NodeWithMeta( + member as TopLevelVariableDeclaration); + + expect(nodeWithMeta.node, same(member)); + expect(nodeWithMeta.metaNode, isNull); + expect(nodeWithMeta.meta, isNull); + }); + }); +} diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/text_util_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/text_util_test.dart new file mode 100644 index 000000000..8ad077542 --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/text_util_test.dart @@ -0,0 +1,125 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +@TestOn('vm') +library; + +import 'package:over_react/src/builder/vendor/transformer_utils/src/text_util.dart'; +import 'package:test/test.dart'; + +main() { + group('stringLiteral()', () { + test('optionally quotes strings', () { + expect(stringLiteral('unquoted', quote: false), 'unquoted'); + expect(stringLiteral('unquoted', useSingleQuote: true, quote: false), + 'unquoted'); + expect(stringLiteral('quoted'), "'quoted'"); + expect(stringLiteral('quoted', useSingleQuote: false), '"quoted"'); + }); + + test('escapes quotes', () { + expect(stringLiteral(r"'", quote: false), r"\'"); + expect(stringLiteral(r'"', useSingleQuote: false, quote: false), r'\"'); + }); + + test('does not escape the other kind of quotes', () { + expect(stringLiteral(r"'", useSingleQuote: false, quote: false), r"'"); + expect(stringLiteral(r'"', quote: false), r'"'); + }); + + test('escapes backslashes', () { + expect(stringLiteral(r'\', quote: false), r'\\'); + }); + + test('escapes dollar signs', () { + expect(stringLiteral(r'$', quote: false), r'\$'); + }); + + test('escapes special characters', () { + // Adapted from dart.convert library's JSON encoder: + // https://github.com/dart-lang/sdk/blob/1.12.0/tests/lib/convert/json_util_test.dart#L138 + // + // + // Copyright 2012, the Dart project authors. All rights reserved. + // Redistribution and use in source and binary forms, with or without + // modification, are permitted provided that the following conditions are + // met: + // * Redistributions of source code must retain the above copyright + // notice, this list of conditions and the following disclaimer. + // * Redistributions in binary form must reproduce the above + // copyright notice, this list of conditions and the following + // disclaimer in the documentation and/or other materials provided + // with the distribution. + // * Neither the name of Google Inc. nor the names of its + // contributors may be used to endorse or promote products derived + // from this software without specific prior written permission. + // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + expect(stringLiteral('\u0000', quote: false), '\\u0000'); + expect(stringLiteral('\u0001', quote: false), '\\u0001'); + expect(stringLiteral('\u0002', quote: false), '\\u0002'); + expect(stringLiteral('\u0003', quote: false), '\\u0003'); + expect(stringLiteral('\u0004', quote: false), '\\u0004'); + expect(stringLiteral('\u0005', quote: false), '\\u0005'); + expect(stringLiteral('\u0006', quote: false), '\\u0006'); + expect(stringLiteral('\u0007', quote: false), '\\u0007'); + expect(stringLiteral('\u0008', quote: false), '\\b'); + expect(stringLiteral('\u0009', quote: false), '\\t'); + expect(stringLiteral('\u000a', quote: false), '\\n'); + expect(stringLiteral('\u000b', quote: false), '\\u000b'); + expect(stringLiteral('\u000c', quote: false), '\\f'); + expect(stringLiteral('\u000d', quote: false), '\\r'); + expect(stringLiteral('\u000e', quote: false), '\\u000e'); + expect(stringLiteral('\u000f', quote: false), '\\u000f'); + expect(stringLiteral('\u0010', quote: false), '\\u0010'); + expect(stringLiteral('\u0011', quote: false), '\\u0011'); + expect(stringLiteral('\u0012', quote: false), '\\u0012'); + expect(stringLiteral('\u0013', quote: false), '\\u0013'); + expect(stringLiteral('\u0014', quote: false), '\\u0014'); + expect(stringLiteral('\u0015', quote: false), '\\u0015'); + expect(stringLiteral('\u0016', quote: false), '\\u0016'); + expect(stringLiteral('\u0017', quote: false), '\\u0017'); + expect(stringLiteral('\u0018', quote: false), '\\u0018'); + expect(stringLiteral('\u0019', quote: false), '\\u0019'); + expect(stringLiteral('\u001a', quote: false), '\\u001a'); + expect(stringLiteral('\u001b', quote: false), '\\u001b'); + expect(stringLiteral('\u001c', quote: false), '\\u001c'); + expect(stringLiteral('\u001d', quote: false), '\\u001d'); + expect(stringLiteral('\u001e', quote: false), '\\u001e'); + expect(stringLiteral('\u001f', quote: false), '\\u001f'); + expect( + stringLiteral('Got \b, \f, \n, \r, \t, \u0000, \\, and ".', + quote: false), + 'Got \\b, \\f, \\n, \\r, \\t, \\u0000, \\\\, and ".'); + expect(stringLiteral('Got \b\f\n\r\t\u0000\\".', quote: false), + 'Got \\b\\f\\n\\r\\t\\u0000\\\\".'); + }); + + test('writes the null literal for an input of null', () { + expect(stringLiteral(null, quote: false), 'null'); + expect(stringLiteral(null), 'null'); + }); + }); +} diff --git a/test/vm_tests/builder/vendor/transformer_utils/unit/transformed_source_file_test.dart b/test/vm_tests/builder/vendor/transformer_utils/unit/transformed_source_file_test.dart new file mode 100644 index 000000000..ded449ec0 --- /dev/null +++ b/test/vm_tests/builder/vendor/transformer_utils/unit/transformed_source_file_test.dart @@ -0,0 +1,245 @@ +// Copyright 2015 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Copied from https://github.com/Workiva/dart_transformer_utils/tree/0.2.23/test/ + +@TestOn('vm') +library; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:over_react/src/builder/vendor/transformer_utils/src/transformed_source_file.dart'; +import 'package:source_span/source_span.dart'; +import 'package:test/test.dart'; + +import '../test_utils.dart'; + + +main() { + group('TransformedSourceFile', () { + late SourceFile testSourceFile; + late TransformedSourceFile transformedFile; + + setUp(() { + testSourceFile = + SourceFile.fromString('0123456789', url: 'test_source_file.dart'); + transformedFile = TransformedSourceFile(testSourceFile); + }); + + group('replace()', () { + test('replaces a span of text and outputs the result', () { + transformedFile.replace(testSourceFile.span(3, 8), '{replaced}'); + expect(transformedFile.getTransformedText(), '012{replaced}89'); + }); + + test('replaces multiple spans of text and outputs the result', () { + transformedFile.replace(testSourceFile.span(1, 2), '{replaced 1}'); + transformedFile.replace(testSourceFile.span(6, 9), '{replaced 2}'); + transformedFile.replace(testSourceFile.span(4, 6), '{replaced 3}'); + expect(transformedFile.getTransformedText(), + '0{replaced 1}23{replaced 3}{replaced 2}9'); + }); + + test('inserts text and outputs the result', () { + transformedFile.insert(testSourceFile.location(4), '{inserted}'); + expect(transformedFile.getTransformedText(), '0123{inserted}456789'); + }); + }); + + group('insert()', () { + test('inserts multiple pieces of text and outputs the result', () { + transformedFile.insert(testSourceFile.location(1), '{inserted 1}'); + transformedFile.insert(testSourceFile.location(6), '{inserted 2}'); + transformedFile.insert(testSourceFile.location(4), '{inserted 3}'); + expect(transformedFile.getTransformedText(), + '0{inserted 1}123{inserted 3}45{inserted 2}6789'); + }); + + test('throws if sections of replaced text overlap each other', () { + transformedFile.replace(testSourceFile.span(1, 3), '{replaced 1}'); + transformedFile.replace(testSourceFile.span(0, 2), '{replaced 2}'); + expect(() => transformedFile.getTransformedText(), + throwsA(hasToStringValue(contains('Overlapping replacement')))); + }); + }); + + group('remove()', () { + const String text = 'line 0\nline 1\nline 2\nline 3\nline 4'; + late SourceSpan lines1Through3Span; + + setUp(() { + testSourceFile = SourceFile.fromString(text); + transformedFile = TransformedSourceFile(testSourceFile); + + var lines1Through3 = '\nline 1\nline 2\nline 3'.allMatches(text).single; + lines1Through3Span = + testSourceFile.span(lines1Through3.start, lines1Through3.end); + }); + + test('removes spans of text, removing newlines by default', () { + transformedFile.remove(lines1Through3Span); + expect(transformedFile.getTransformedText(), 'line 0\nline 4'); + }); + + test('removes spans of text, preserving newlines when specified', () { + transformedFile.remove(lines1Through3Span, preserveNewlines: true); + expect(transformedFile.getTransformedText(), 'line 0\n\n\n\nline 4'); + }); + }); + + group('`isModified`', () { + test('is true when a replacement has been made', () { + transformedFile.replace(testSourceFile.span(3, 8), '{replaced}'); + expect(transformedFile.isModified, isTrue); + }); + + test('is true when an insertion has been made', () { + transformedFile.insert(testSourceFile.location(3), '{replaced}'); + expect(transformedFile.isModified, isTrue); + }); + + test('is false for a pristine TransformedSourceFile', () { + expect(transformedFile.isModified, isFalse); + }); + }); + + group('iterateReplacements()', () { + test('iterates through the replacements using the specified callbacks', + () { + var unmodifieds = []; + var additions = []; + var removals = []; + + var all = []; + + transformedFile.remove(testSourceFile.span(2, 3)); + transformedFile.remove(testSourceFile.span(8, 9)); + transformedFile.insert(testSourceFile.location(1), '{inserted 1}'); + transformedFile.insert(testSourceFile.location(7), '{inserted 2}'); + transformedFile.replace(testSourceFile.span(5, 7), '{replaced 1}'); + transformedFile.replace(testSourceFile.span(0, 1), '{replaced 2}'); + + transformedFile.iterateReplacements(onUnmodified: (text) { + unmodifieds.add(text); + all.add(text); + }, onRemoval: (text) { + removals.add(text); + all.add(text); + }, onAddition: (text) { + additions.add(text); + all.add(text); + }); + + expect(unmodifieds, equals(['1', '34', '7', '9'])); + expect( + additions, + equals([ + '{replaced 2}', + '{inserted 1}', + '{replaced 1}', + '{inserted 2}' + ])); + expect(removals, equals(['0', '2', '56', '8'])); + + expect( + all, + equals([ + '0', // - removal + '{replaced 2}', // + addition + '{inserted 1}', // + addition + '1', // unmodified + '2', // - removal + '34', // unmodified + '56', // - removal + '{replaced 1}', // + addition + '{inserted 2}', // + addition + '7', // unmodified + '8', // - removal + '9' // unmodified + ]), + reason: 'should have called all parts in the right order'); + }); + }); + + group('getHtmlDiff()', () { + test('returns an HTML page containing the diff of the modified file', () { + transformedFile.remove(testSourceFile.span(2, 3)); + transformedFile.remove(testSourceFile.span(8, 9)); + transformedFile.insert(testSourceFile.location(1), '{inserted 1}'); + transformedFile.insert(testSourceFile.location(7), '{inserted 2}'); + transformedFile.replace(testSourceFile.span(5, 7), '{replaced 1}'); + transformedFile.replace(testSourceFile.span(0, 1), '{replaced 2}'); + + var html = transformedFile.getHtmlDiff(); + + expect(html, matches(RegExp(r'^\s*<\!DOCTYPE html>\s*'))); + expect( + html, + contains('Transformer Diff - ' + 'test_source_file.dart' + '')); + expect( + html, + contains('
'
+                '0'
+                '{replaced 2}'
+                '{inserted 1}'
+                '1'
+                '2'
+                '34'
+                '56'
+                '{replaced 1}'
+                '{inserted 2}'
+                '7'
+                '8'
+                '9'
+                '
')); + expect(html, matches(RegExp(r'\s*$'))); + }); + }); + }); + + group('getSpan()', () { + const String source = '\n' + '/// Doc comment\n' + '@Annotation\n' + 'class Node {}\n'; + + late SourceFile sourceFile; + late AnnotatedNode node; + + setUp(() { + sourceFile = SourceFile.fromString(source); + node = parseString(content: source, throwIfDiagnostics: false) + .unit + .declarations + .single; + }); + + test( + 'returns a SourceSpan for a node, skipping its doc comments and meta by default', + () { + var span = getSpan(sourceFile, node); + expect(span.start.offset, equals(source.indexOf('class'))); + expect(span.end.offset, equals(source.indexOf('}') + 1)); + }); + + test('returns a SourceSpan for a node, including its doc comments and meta', + () { + var span = getSpan(sourceFile, node, skipCommentAndMetadata: false); + expect(span.start.offset, equals(source.indexOf('///'))); + expect(span.end.offset, equals(source.indexOf('}') + 1)); + }); + }); +}