From 731e6d38118d09a1456713f8cbabda0c709b851c Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Sun, 18 Dec 2016 16:40:40 -0800 Subject: [PATCH] Initial commit. --- .analysis_options | 28 +++++ .gitignore | 3 + bin/deduplicate.dart | 13 +++ lib/source_transformer.dart | 11 ++ lib/src/cli.dart | 25 +++++ lib/src/dart/directives.dart | 123 +++++++++++++++++++++ lib/src/dart/directives/equality.dart | 39 +++++++ lib/src/dart/directives/transformer.dart | 38 +++++++ lib/src/patch.dart | 120 ++++++++++++++++++++ lib/src/phase.dart | 30 +++++ lib/src/transformer.dart | 78 +++++++++++++ lib/src/utils.dart | 10 ++ pubspec.yaml | 11 ++ test/dart/directives/deduplicate_test.dart | 42 +++++++ test/dart/directives/equality_test.dart | 58 ++++++++++ test/dart/directives/remove_test.dart | 23 ++++ test/dart/directives/replace_test.dart | 24 ++++ test/patch_test.dart | 32 ++++++ test/phase_test.dart | 77 +++++++++++++ 19 files changed, 785 insertions(+) create mode 100644 .analysis_options create mode 100644 .gitignore create mode 100644 bin/deduplicate.dart create mode 100644 lib/source_transformer.dart create mode 100644 lib/src/cli.dart create mode 100644 lib/src/dart/directives.dart create mode 100644 lib/src/dart/directives/equality.dart create mode 100644 lib/src/dart/directives/transformer.dart create mode 100644 lib/src/patch.dart create mode 100644 lib/src/phase.dart create mode 100644 lib/src/transformer.dart create mode 100644 lib/src/utils.dart create mode 100644 pubspec.yaml create mode 100644 test/dart/directives/deduplicate_test.dart create mode 100644 test/dart/directives/equality_test.dart create mode 100644 test/dart/directives/remove_test.dart create mode 100644 test/dart/directives/replace_test.dart create mode 100644 test/patch_test.dart create mode 100644 test/phase_test.dart diff --git a/.analysis_options b/.analysis_options new file mode 100644 index 0000000..03b7330 --- /dev/null +++ b/.analysis_options @@ -0,0 +1,28 @@ +analyzer: + strong-mode: true +linter: + rules: + # Errors + - avoid_empty_else + - control_flow_in_finally + - empty_statements + - test_types_in_equals + - throw_in_finally + - valid_regexps + + # Style + - annotate_overrides + - avoid_init_to_null + - avoid_return_types_on_setters + - await_only_futures + - camel_case_types + - comment_references + - empty_catches + - empty_constructor_bodies + - hash_and_equals + - library_prefixes + - non_constant_identifier_names + - prefer_is_not_empty + - slash_for_doc_comments + - type_init_formals + - unrelated_type_equality_checks \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1d79863 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.packages +.pub +pubspec.lock diff --git a/bin/deduplicate.dart b/bin/deduplicate.dart new file mode 100644 index 0000000..17d5ed9 --- /dev/null +++ b/bin/deduplicate.dart @@ -0,0 +1,13 @@ +import 'package:source_transformer/source_transformer.dart'; + +/// Remove all duplicate imports and exports in a set of files. +/// +/// Simple but useful example of consuming a source transformer. +/// +/// ## Example use +/// ``` +/// $ dart bin/deduplicate.dart /some/path.dart /some/other/path.dart ... +/// ``` +main(List paths) async { + await runTransformer(const DeduplicateDirectives(), paths); +} diff --git a/lib/source_transformer.dart b/lib/source_transformer.dart new file mode 100644 index 0000000..2206598 --- /dev/null +++ b/lib/source_transformer.dart @@ -0,0 +1,11 @@ +export 'package:source_transformer/src/cli.dart' show runTransformer; +export 'package:source_transformer/src/dart/directives.dart' + show + DeduplicateDirectives, + DirectiveTransformer, + RemoveDirectives, + ReplaceDirectives; +export 'package:source_transformer/src/patch.dart' show Patch; +export 'package:source_transformer/src/phase.dart' show Phase; +export 'package:source_transformer/src/transformer.dart' + show DartSourceTransformer, Transformer; diff --git a/lib/src/cli.dart b/lib/src/cli.dart new file mode 100644 index 0000000..e989a16 --- /dev/null +++ b/lib/src/cli.dart @@ -0,0 +1,25 @@ +// Copyright (c) 2016, 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 'dart:async'; +import 'dart:io'; + +import 'package:source_span/source_span.dart'; +import 'package:source_transformer/source_transformer.dart'; + +/// Convenience function to run a [transformer] on the provided file [paths]. +/// +/// See `bin/deduplicate.dart` for an example. +Future runTransformer( + Transformer transformer, + Iterable paths, +) { + return Future.wait(paths.map((path) async { + final input = new File(path); + final sourceText = input.readAsStringSync(); + final oldFile = new SourceFile(sourceText, url: path); + final newFile = await transformer.transform(oldFile); + input.writeAsStringSync(newFile.getText(0)); + })); +} diff --git a/lib/src/dart/directives.dart b/lib/src/dart/directives.dart new file mode 100644 index 0000000..6c64f82 --- /dev/null +++ b/lib/src/dart/directives.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2016, 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. +library source_transformer.src.dart.directives; + +import 'package:analyzer/analyzer.dart'; +import 'package:analyzer/dart/ast/standard_ast_factory.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/src/dart/ast/token.dart'; +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; +import 'package:source_transformer/source_transformer.dart'; +import 'package:source_transformer/src/utils.dart'; + +part 'directives/equality.dart'; +part 'directives/transformer.dart'; + +/// A transformer that removes duplicate import and export directives. +/// +/// The default implementation of `Equality` only attempts to remove +/// directives without deferred loading, an `as` statement, or the `show` and +/// `hide` combinators. +/// +/// An ideal transformer to run after refactoring directives. +class DeduplicateDirectives extends DirectiveTransformer> { + final Equality _equality; + + @literal + const DeduplicateDirectives({ + Equality equality: const DirectiveEquality(), + }) + : _equality = equality; + + @override + bool canTransform(Directive directive) { + if (directive is ImportDirective) { + return directive.asKeyword == null && + directive.deferredKeyword == null && + directive.combinators == null || + directive.combinators.isEmpty; + } + if (directive is ExportDirective) { + return directive.combinators == null || directive.combinators.isEmpty; + } + return false; + } + + @override + Set defaultState() => new EqualitySet(_equality); + + @override + Directive transformDirective(Directive directive, [Set state]) { + return state.add(directive) ? directive : null; + } +} + +/// A source transformer that removes directives that reference certain URIs. +/// +/// If a URI is not found, it is skipped. +/// +/// ## Example use +/// ``` +/// const RemoveDirectives([ +/// 'package:a/a.dart', +/// 'package:b/b.dart', +/// ]) +/// ``` +class RemoveDirectives extends DirectiveTransformer { + final Iterable _uris; + + @literal + const RemoveDirectives(this._uris); + + @mustCallSuper + @override + bool canTransform(Directive directive) { + if (directive is UriBasedDirective) { + return _uris.contains(directive.uri.stringValue); + } + return false; + } + + @override + Directive transformDirective(@checked UriBasedDirective directive, [_]) { + return null; + } +} + +/// A source transformer that replaces directives with other URIs. +/// +/// ## Example use +/// ``` +/// const ReplaceDirectives({ +/// 'package:a/a.dart': 'package:b/b.dart', +/// }) +/// ``` +class ReplaceDirectives extends DirectiveTransformer { + final Map _uris; + + const ReplaceDirectives(this._uris); + + @mustCallSuper + @override + bool canTransform(Directive directive) { + if (directive is UriBasedDirective) { + return _uris.containsKey(directive.uri.stringValue); + } + return false; + } + + @override + Directive transformDirective(@checked UriBasedDirective directive, [_]) { + final newUri = _uris[directive.uri.stringValue]; + if (newUri != null) { + return cloneNode(directive) + ..uri = astFactory.simpleStringLiteral( + new StringToken(TokenType.STRING, "'$newUri'", 0), + "'$newUri'", + ); + } + return directive; + } +} diff --git a/lib/src/dart/directives/equality.dart b/lib/src/dart/directives/equality.dart new file mode 100644 index 0000000..b4207a3 --- /dev/null +++ b/lib/src/dart/directives/equality.dart @@ -0,0 +1,39 @@ +// Copyright (c) 2016, 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. +part of source_transformer.src.dart.directives; + +class DirectiveEquality implements Equality { + const DirectiveEquality(); + + @override + bool equals(Directive e1, Directive e2) { + if (e1 is ImportDirective && e2 is ImportDirective) { + if (e1.asKeyword != null || + e1.deferredKeyword != null || + e1.combinators?.isNotEmpty == true || + e2.asKeyword != null || + e2.deferredKeyword != null || + e2.combinators?.isNotEmpty == true) { + // For now don't try to compare import directive properties. + return false; + } + return e1.uri.stringValue == e2.uri.stringValue; + } + if (e1 is ExportDirective && e2 is ExportDirective) { + if (e1.combinators?.isNotEmpty == true || + e2.combinators?.isNotEmpty == true) { + // For now don't try to compare import directive properties. + return false; + } + return e1.uri.stringValue == e2.uri.stringValue; + } + return false; + } + + @override + int hash(Directive e) => e.toSource().hashCode; + + @override + bool isValidKey(Object o) => o is Directive; +} \ No newline at end of file diff --git a/lib/src/dart/directives/transformer.dart b/lib/src/dart/directives/transformer.dart new file mode 100644 index 0000000..8f80930 --- /dev/null +++ b/lib/src/dart/directives/transformer.dart @@ -0,0 +1,38 @@ +// Copyright (c) 2016, 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. +part of source_transformer.src.dart.directives; + +/// A high-level transformer for manipulating `import` and `export` directives. +abstract class DirectiveTransformer extends DartSourceTransformer { + const DirectiveTransformer(); + + /// Should return `true` if the directive should be altered. + @protected + bool canTransform(Directive directive); + + /// Return a new default state object; optional. + @protected + S defaultState() => null; + + @override + Iterable computePatches(CompilationUnit parsedSource) { + final initialState = defaultState(); + return parsedSource.directives.where(canTransform).map((oldDirective) { + final newDirective = transformDirective(oldDirective, initialState); + if (newDirective == null) { + return new Patch.removeAst(oldDirective); + } + return new Patch.replaceAst(oldDirective, newDirective); + }); + } + + /// Returns a new directive to replace [directive] with. + /// + /// May return back the _same_ directive to avoid doing anything, but if + /// possible it is preferred to make this type of check in [canTransform]. + /// + /// If `null` is returned, indicates that directive should be removed. + @protected + Directive transformDirective(Directive directive, [S state]); +} diff --git a/lib/src/patch.dart b/lib/src/patch.dart new file mode 100644 index 0000000..cb1bd2a --- /dev/null +++ b/lib/src/patch.dart @@ -0,0 +1,120 @@ +// Copyright (c) 2016, 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:analyzer/analyzer.dart'; +import 'package:meta/meta.dart'; +import 'package:quiver/core.dart'; + +/// A patch that may be applied on existing source file. +/// +/// Existing text between [offset] and [length] should be replaced with [text]. +abstract class Patch implements Comparable { + const Patch._(); + + /// Returns a simple patch that replaces existing text. + @literal + const factory Patch( + int offset, + int length, + String text, + ) = _StringPatch; + + /// Returns a patch that makes no modifications to a file. + @literal + const factory Patch.identity() = _IdentityPatch; + + /// Returns a patch that removes the AST [oldAst]. + @literal + const factory Patch.removeAst(AstNode oldAst) = _RemoveAstPatch; + + /// Returns a patch that replaces the AST [oldAst] with [newAst]. + @literal + const factory Patch.replaceAst( + AstNode oldAst, + AstNode newAst, + ) = _ReplaceAstPatch; + + @override + bool operator ==(Object o) { + if (o is Patch) { + return offset == o.offset && length == o.length && text == o.text; + } + return false; + } + + @override + int get hashCode => hash3(offset, length, text); + + @override + int compareTo(Patch patch) => offset.compareTo(patch.offset); + + /// Where in the source file the patch should be applied. + int get offset; + + /// Starting at [offset], how many character should be removed. + int get length; + + /// After removing [length] characters at [offset], replace with this value. + String get text; + + @override + String toString() => '$Patch {$offset->${offset + length}: $text}'; +} + +class _IdentityPatch extends Patch { + const _IdentityPatch() : super._(); + + @override + final int offset = 0; + + @override + final int length = 0; + + @override + final String text = ''; +} + +class _RemoveAstPatch extends Patch { + final AstNode _oldNode; + + const _RemoveAstPatch(this._oldNode) : super._(); + + @override + int get offset => _oldNode.offset; + + @override + int get length => _oldNode.length; + + @override + final String text = ''; +} + +class _ReplaceAstPatch extends Patch { + final AstNode _oldNode; + final AstNode _newNode; + + const _ReplaceAstPatch(this._oldNode, this._newNode) : super._(); + + @override + int get offset => _oldNode.offset; + + @override + int get length => _oldNode.length; + + @override + String get text => _newNode.toSource(); +} + +class _StringPatch extends Patch { + const _StringPatch(this.offset, this.length, this.text) : super._(); + + @override + final int offset; + + @override + final int length; + + @override + final String text; +} diff --git a/lib/src/phase.dart b/lib/src/phase.dart new file mode 100644 index 0000000..641c14e --- /dev/null +++ b/lib/src/phase.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2016, 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:collection/collection.dart'; +import 'package:source_transformer/src/patch.dart'; + +/// A synchronous phase of multiple, ordered patches to a source file. +abstract class Phase implements Iterable { + /// Create a new phase from an existing set of [patches]. + /// + /// Patches are ordered to apply in decreasing order of offsets. + factory Phase(Iterable patches) { + return new _ListPhase((patches.toList()..sort()).reversed.toList()); + } + + /// Returns a new source text by patching [text]. + String apply(String text) { + return fold(text, (text, p) { + return text.replaceRange(p.offset, p.offset + p.length, p.text); + }); + } +} + +class _ListPhase extends DelegatingList with Phase { + _ListPhase(List patches) : super(patches); + + @override + String toString() => '$Phase {$this}'; +} diff --git a/lib/src/transformer.dart b/lib/src/transformer.dart new file mode 100644 index 0000000..0ca4ce7 --- /dev/null +++ b/lib/src/transformer.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2016, 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 'dart:async'; + +import 'package:analyzer/analyzer.dart'; +import 'package:meta/meta.dart'; +import 'package:source_span/source_span.dart'; +import 'package:source_transformer/src/patch.dart'; +import 'package:source_transformer/src/phase.dart'; + +/// A transforming API that applies a set of changes to an input file. +/// +/// To apply multiple transformers, see [Transformer.multiple]. +abstract class Transformer { + /// Returns a source transformer that applies multiple [transformers]. + @literal + const factory Transformer.multiple( + Iterable transformers, + ) = _MultiTransformer; + + /// Returns a source transformer that just returns the original file. + @literal + const factory Transformer.identity() = _IdentityTransformer; + + /// Returns a future that completes with a transformed [file]. + Future transform(SourceFile file); +} + +/// A base transformer that may be implemented to easily patch `.dart` files. +abstract class DartSourceTransformer implements Transformer { + final bool _parseFunctionBodies; + + const DartSourceTransformer({bool parseFunctionBodies: false}) + : _parseFunctionBodies = parseFunctionBodies; + + /// Returns a collection of patches to apply on [parsedSource]. + @protected + Iterable computePatches(CompilationUnit parsedSource); + + @override + Future transform(SourceFile file) async { + final parsedSource = parseCompilationUnit( + file.getText(0), + parseFunctionBodies: _parseFunctionBodies, + ); + return new SourceFile( + new Phase(computePatches(parsedSource)).apply( + file.getText(0), + ), + url: file.url, + ); + } +} + +class _IdentityTransformer implements Transformer { + const _IdentityTransformer(); + + @override + Future transform(SourceFile file) async => file; +} + +class _MultiTransformer implements Transformer { + final Iterable _transformers; + + const _MultiTransformer(this._transformers); + + @override + Future transform(SourceFile file) { + return new Stream.fromIterable(_transformers).fold/*>*/( + new Future.value(file), + (futureFile, transformer) async { + return transformer.transform(await futureFile); + }, + ); + } +} diff --git a/lib/src/utils.dart b/lib/src/utils.dart new file mode 100644 index 0000000..2d8d86e --- /dev/null +++ b/lib/src/utils.dart @@ -0,0 +1,10 @@ +// Copyright (c) 2016, 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:analyzer/analyzer.dart'; + +/// Returns a deep-copy of [astNode]. +/*=T*/ cloneNode/**/(AstNode/*=T*/ astNode) { + return new AstCloner().cloneNode/**/(astNode); +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..b1e974e --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,11 @@ +name: source_transformer + +dependencies: + analyzer: + collection: + meta: + quiver: + source_span: + +dev_dependencies: + test: diff --git a/test/dart/directives/deduplicate_test.dart b/test/dart/directives/deduplicate_test.dart new file mode 100644 index 0000000..c6924d0 --- /dev/null +++ b/test/dart/directives/deduplicate_test.dart @@ -0,0 +1,42 @@ +import 'package:source_span/source_span.dart'; +import 'package:source_transformer/src/dart/directives.dart'; +import 'package:test/test.dart'; + +void main() { + test('should deduplicate directives', () async { + final file = new SourceFile(r''' + import 'package:bar/bar.dart'; + import 'package:bar/bar.dart'; + import 'package:bar/bar.dart' as bar; + import 'package:bar/bar.dart' as bar; + import 'package:bar/bar.dart' show bar; + import 'package:bar/bar.dart' show bar; + import 'package:bar/bar.dart' hide bar; + import 'package:bar/bar.dart' hide bar; + export 'package:bar/bar.dart'; + export 'package:bar/bar.dart'; + export 'package:bar/bar.dart' show bar; + export 'package:bar/bar.dart' show bar; + export 'package:bar/bar.dart' hide bar; + export 'package:bar/bar.dart' hide bar; + '''); + final result = await const DeduplicateDirectives().transform(file); + expect( + result.getText(0), + equalsIgnoringWhitespace(r''' + import 'package:bar/bar.dart'; + import 'package:bar/bar.dart' as bar; + import 'package:bar/bar.dart' as bar; + import 'package:bar/bar.dart' show bar; + import 'package:bar/bar.dart' show bar; + import 'package:bar/bar.dart' hide bar; + import 'package:bar/bar.dart' hide bar; + export 'package:bar/bar.dart'; + export 'package:bar/bar.dart' show bar; + export 'package:bar/bar.dart' show bar; + export 'package:bar/bar.dart' hide bar; + export 'package:bar/bar.dart' hide bar; + '''), + ); + }); +} diff --git a/test/dart/directives/equality_test.dart b/test/dart/directives/equality_test.dart new file mode 100644 index 0000000..f6180ea --- /dev/null +++ b/test/dart/directives/equality_test.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/analyzer.dart'; +import 'package:source_transformer/src/dart/directives.dart'; +import 'package:test/test.dart'; + +void main() { + const equality = const DirectiveEquality(); + List imports; + List exports; + + setUpAll(() { + imports = parseDirectives(r''' + import 'package:foo/foo.dart'; // 0 + import 'package:foo/foo.dart'; // 1 + import 'package:bar/bar.dart'; // 2 + import 'package:bar/bar.dart' as bar; // 3 + import 'package:bar/bar.dart' as bar; // 4 + import 'package:bar/bar.dart' as bar2; // 5 + import 'package:baz/baz.dart'; // 6 + import 'package:baz/baz.dart' show baz; // 7 + import 'package:baz/baz.dart' show baz; // 8 + import 'package:baz/baz.dart' hide baz; // 9 + import 'package:baz/baz.dart' hide baz; // 10 + ''').directives.where((d) => d is ImportDirective).toList(); + + exports = parseDirectives(r''' + export 'package:foo/foo.dart'; // 0 + export 'package:foo/foo.dart'; // 1 + export 'package:bar/bar.dart'; // 2 + export 'package:baz/baz.dart'; // 3 + export 'package:baz/baz.dart' show baz; // 4 + export 'package:baz/baz.dart' show baz; // 5 + export 'package:baz/baz.dart' hide baz; // 6 + export 'package:baz/baz.dart' hide baz; // 7 + ''').directives.where((d) => d is ExportDirective).toList(); + }); + + // Currently only supports URI comparisons, so test cases limited. + + group('$ImportDirective', () { + test('should be equivalent to another import directive', () { + expect(equality.equals(imports[0], imports[1]), isTrue); + }); + + test('should not be equuivalent with a different URI', () { + expect(equality.equals(imports[0], imports[2]), isFalse); + }); + }); + + group('$ExportDirective', () { + test('should be equivalent to another import directive', () { + expect(equality.equals(exports[0], exports[1]), isTrue); + }); + + test('should not be equuivalent with a different URI', () { + expect(equality.equals(exports[2], exports[3]), isFalse); + }); + }); +} diff --git a/test/dart/directives/remove_test.dart b/test/dart/directives/remove_test.dart new file mode 100644 index 0000000..a596015 --- /dev/null +++ b/test/dart/directives/remove_test.dart @@ -0,0 +1,23 @@ +import 'package:source_span/source_span.dart'; +import 'package:source_transformer/src/dart/directives.dart'; +import 'package:test/test.dart'; + +void main() { + test('should remove directives', () async { + final file = new SourceFile(r''' + import 'package:foo/foo.dart'; + import 'package:bar/bar.dart'; + import 'package:baz/baz.dart'; + '''); + final result = await const RemoveDirectives(const [ + 'package:bar/bar.dart', + ]).transform(file); + expect( + result.getText(0), + equalsIgnoringWhitespace(r''' + import 'package:foo/foo.dart'; + import 'package:baz/baz.dart'; + '''), + ); + }); +} diff --git a/test/dart/directives/replace_test.dart b/test/dart/directives/replace_test.dart new file mode 100644 index 0000000..04b70b0 --- /dev/null +++ b/test/dart/directives/replace_test.dart @@ -0,0 +1,24 @@ +import 'package:source_span/source_span.dart'; +import 'package:source_transformer/src/dart/directives.dart'; +import 'package:test/test.dart'; + +void main() { + test('should replace directives', () async { + final file = new SourceFile(r''' + import 'package:foo/foo.dart'; + import 'package:bar/bar.dart'; + import 'package:baz/baz.dart'; + '''); + final result = await const ReplaceDirectives(const { + 'package:bar/bar.dart': 'package:bar/deprecated.dart', + }).transform(file); + expect( + result.getText(0), + equalsIgnoringWhitespace(r''' + import 'package:foo/foo.dart'; + import 'package:bar/deprecated.dart'; + import 'package:baz/baz.dart'; + '''), + ); + }); +} diff --git a/test/patch_test.dart b/test/patch_test.dart new file mode 100644 index 0000000..e653e92 --- /dev/null +++ b/test/patch_test.dart @@ -0,0 +1,32 @@ +// Copyright (c) 2016, 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:analyzer/analyzer.dart'; +import 'package:source_transformer/src/patch.dart'; +import 'package:test/test.dart'; + +void main() { + test('should create a simple text patch', () { + final patch = new Patch(10, 5, 'Test'); + expect(patch.offset, 10); + expect(patch.length, 5); + expect(patch.text, 'Test'); + }); + + test('should create a simple AST removal patch', () { + final patch = new Patch.removeAst(parseCompilationUnit('class A {}')); + expect(patch.offset, 0); + expect(patch.length, 10); + expect(patch.text, isEmpty); + }); + + test('should create a simple AST replacement patch', () { + final astA = parseCompilationUnit('class A {}'); + final astB = parseCompilationUnit('class B {}'); + final patch = new Patch.replaceAst(astA, astB); + expect(patch.offset, 0); + expect(patch.length, 10); + expect(patch.text, astB.toSource()); + }); +} diff --git a/test/phase_test.dart b/test/phase_test.dart new file mode 100644 index 0000000..2ee0639 --- /dev/null +++ b/test/phase_test.dart @@ -0,0 +1,77 @@ +// Copyright (c) 2016, 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:analyzer/analyzer.dart'; +import 'package:analyzer/dart/ast/standard_ast_factory.dart'; +import 'package:analyzer/dart/ast/token.dart'; +import 'package:analyzer/src/dart/ast/token.dart'; +import 'package:source_transformer/src/patch.dart'; +import 'package:source_transformer/src/phase.dart'; +import 'package:source_transformer/src/utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('should apply a simple string replacement', () { + final string = 'Hello "Joe"!'; + expect( + new Phase([ + new Patch(string.indexOf('"') + 1, 'Joe'.length, 'Jill'), + ]).apply(string), + 'Hello "Jill"!', + ); + }); + + test('should apply a simple AST removal', () { + const file = ''' + import 'package:old/old.dart'; + + void main() { + doThing(); + } + '''; + + final oldDirective = parseCompilationUnit(file).directives.first; + + expect( + new Phase([ + new Patch.removeAst(oldDirective), + ]).apply(file), + equalsIgnoringWhitespace(r''' + void main() { + doThing(); + } + '''), + ); + }); + + test('should apply a simple AST replacement', () { + const file = ''' + import 'package:old/old.dart'; + + void main() { + doThing(); + } + '''; + + final oldDirective = parseCompilationUnit(file).directives.first; + final newDirective = cloneNode/**/(oldDirective); + newDirective.uri = astFactory.simpleStringLiteral( + new StringToken(TokenType.STRING, "'package:new/new.dart'", 0), + "'package:new/new.dart'", + ); + + expect( + new Phase([ + new Patch.replaceAst(oldDirective, newDirective), + ]).apply(file), + equalsIgnoringWhitespace(r''' + import 'package:new/new.dart'; + + void main() { + doThing(); + } + '''), + ); + }); +}