| // Copyright (c) 2024, 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:dev_compiler/src/kernel/hot_reload_delta_inspector.dart'; | 
 | import 'package:kernel/ast.dart'; | 
 | import 'package:kernel/library_index.dart'; | 
 | import 'package:test/test.dart'; | 
 |  | 
 | import 'memory_compiler.dart'; | 
 |  | 
 | Future<void> main() async { | 
 |   group('const classes', () { | 
 |     final deltaInspector = HotReloadDeltaInspector(); | 
 |     test('rejection when removing only const constructor', () async { | 
 |       final initialSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             const A(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalVariable = const A('hello'); | 
 |             print(globalVariable.s); | 
 |           } | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             A(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Const class cannot become non-const: ' | 
 |               "Library:'memory:///main.dart' " | 
 |               'Class: A', | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |     test('multiple rejections when removing only const constructors', () async { | 
 |       final initialSource = ''' | 
 |           var globalA, globalB, globalC, globalD; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             const A(this.s); | 
 |           } | 
 |  | 
 |           class B { | 
 |             final String s; | 
 |             const B(this.s); | 
 |           } | 
 |  | 
 |           class C { | 
 |             final String s; | 
 |             C(this.s); | 
 |           } | 
 |  | 
 |           class D { | 
 |             final String s; | 
 |             const D(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalA = const A('hello'); | 
 |             globalB = const B('world'); | 
 |             globalC = C('hello'); | 
 |             globalD = const D('world'); | 
 |             print(globalA.s); | 
 |             print(globalB.s); | 
 |             print(globalC.s); | 
 |             print(globalD.s); | 
 |           } | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           var globalA, globalB, globalC, globalD; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             A(this.s); | 
 |           } | 
 |  | 
 |           class B { | 
 |             final String s; | 
 |             const B(this.s); | 
 |           } | 
 |  | 
 |           class C { | 
 |             final String s; | 
 |             C(this.s); | 
 |           } | 
 |  | 
 |           class D { | 
 |             final String s; | 
 |             D(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Const class cannot become non-const: ' | 
 |               "Library:'memory:///main.dart' " | 
 |               'Class: A', | 
 |           'Const class cannot become non-const: ' | 
 |               "Library:'memory:///main.dart' " | 
 |               'Class: D', | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |  | 
 |     test( | 
 |       'no error when removing const constructor while adding another', | 
 |       () async { | 
 |         final initialSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             const A(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalVariable = const A('hello'); | 
 |             print(globalVariable.s); | 
 |           } | 
 |           '''; | 
 |         final deltaSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s; | 
 |             A(this.s); | 
 |             const A.named(this.s); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |         final (:initial, :delta) = await compileComponents( | 
 |           initialSource, | 
 |           deltaSource, | 
 |         ); | 
 |         expect(deltaInspector.compareGenerations(initial, delta), isEmpty); | 
 |       }, | 
 |     ); | 
 |     test('rejection when removing a field', () async { | 
 |       final initialSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t, w; | 
 |             const A(this.s, this.t, this.w); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalVariable = const A('hello', 'world', '!'); | 
 |             print(globalVariable.s); | 
 |           } | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t; | 
 |             const A(this.s, this.t); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Const class cannot remove fields: ' | 
 |               "Library:'memory:///main.dart' Class: A", | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |     test('rejection when removing a field while adding another', () async { | 
 |       final initialSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t, w; | 
 |             const A(this.s, this.t, this.w); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalVariable = const A('hello', 'world', '!'); | 
 |             print(globalVariable.s); | 
 |           } | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t, x; | 
 |             const A(this.s, this.t, this.x); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Const class cannot remove fields: ' | 
 |               "Library:'memory:///main.dart' Class: A", | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |     test( | 
 |       'no error when removing field while also making class const', | 
 |       () async { | 
 |         final initialSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t, w; | 
 |             A(this.s, this.t, this.w); | 
 |           } | 
 |  | 
 |           main() { | 
 |             globalVariable = A('hello', 'world', '!'); | 
 |             print(globalVariable.s); | 
 |           } | 
 |           '''; | 
 |         final deltaSource = ''' | 
 |           var globalVariable; | 
 |  | 
 |           class A { | 
 |             final String s, t; | 
 |             const A(this.s, this.t); | 
 |           } | 
 |  | 
 |           main() { | 
 |             print('hello world'); | 
 |           } | 
 |           '''; | 
 |         final (:initial, :delta) = await compileComponents( | 
 |           initialSource, | 
 |           deltaSource, | 
 |         ); | 
 |         expect( | 
 |           () => deltaInspector.compareGenerations(initial, delta), | 
 |           returnsNormally, | 
 |         ); | 
 |       }, | 
 |     ); | 
 |   }); | 
 |   group('deleted top level members appear in delta library metadata', () { | 
 |     final deltaInspector = HotReloadDeltaInspector(); | 
 |     test('method', () async { | 
 |       final initialSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |  | 
 |           void deleted() {} | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         () => deltaInspector.compareGenerations(initial, delta), | 
 |         returnsNormally, | 
 |       ); | 
 |       final repo = | 
 |           delta.metadata[hotReloadLibraryMetadataTag] | 
 |               as HotReloadLibraryMetadataRepository; | 
 |       repo.mapToIndexedNodes(LibraryIndex.all(delta)); | 
 |       final metadata = | 
 |           repo.mapping[delta.libraries.firstWhere( | 
 |             (l) => l.importUri.toString() == 'memory:///main.dart', | 
 |           )]!; | 
 |       expect(metadata.deletedStaticProcedureNames, orderedEquals(['deleted'])); | 
 |     }); | 
 |     test('getter', () async { | 
 |       final initialSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |  | 
 |           dynamic get deletedGetter => null; | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         () => deltaInspector.compareGenerations(initial, delta), | 
 |         returnsNormally, | 
 |       ); | 
 |       final repo = | 
 |           delta.metadata[hotReloadLibraryMetadataTag] | 
 |               as HotReloadLibraryMetadataRepository; | 
 |       repo.mapToIndexedNodes(LibraryIndex.all(delta)); | 
 |       final metadata = | 
 |           repo.mapping[delta.libraries.firstWhere( | 
 |             (l) => l.importUri.toString() == 'memory:///main.dart', | 
 |           )]!; | 
 |       expect( | 
 |         metadata.deletedStaticProcedureNames, | 
 |         orderedEquals(['deletedGetter']), | 
 |       ); | 
 |     }); | 
 |     test('setter', () async { | 
 |       final initialSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |  | 
 |           set deletedSetter(dynamic value) {} | 
 |           '''; | 
 |       final deltaSource = ''' | 
 |           void retainedMethod() {} | 
 |  | 
 |           dynamic get retainedGetter => null; | 
 |  | 
 |           set retainedSetter(dynamic value) {} | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |       ); | 
 |       expect( | 
 |         () => deltaInspector.compareGenerations(initial, delta), | 
 |         returnsNormally, | 
 |       ); | 
 |       final repo = | 
 |           delta.metadata[hotReloadLibraryMetadataTag] | 
 |               as HotReloadLibraryMetadataRepository; | 
 |       repo.mapToIndexedNodes(LibraryIndex.all(delta)); | 
 |       final metadata = | 
 |           repo.mapping[delta.libraries.firstWhere( | 
 |             (l) => l.importUri.toString() == 'memory:///main.dart', | 
 |           )]!; | 
 |       expect( | 
 |         metadata.deletedStaticProcedureNames, | 
 |         orderedEquals(['deletedSetter']), | 
 |       ); | 
 |     }); | 
 |   }); | 
 |  | 
 |   group('Non-hot-reloadable packages ', () { | 
 |     final packageName = 'test_package'; | 
 |  | 
 |     final deltaInspector = HotReloadDeltaInspector( | 
 |       nonHotReloadablePackages: {packageName}, | 
 |     ); | 
 |     test('reject reloads when a member is added.', () async { | 
 |       final initialAndDeltaSource = | 
 |           ''' | 
 |           import 'package:$packageName/file.dart'; | 
 |           main() {} | 
 |           '''; | 
 |       final initialPackageSource = 'class Foo {}'; | 
 |       final deltaPackageSource = 'class Foo { int member = 100; }'; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialAndDeltaSource, | 
 |         initialAndDeltaSource, | 
 |         initialPackageSource: initialPackageSource, | 
 |         deltaPackageSource: deltaPackageSource, | 
 |         packageName: packageName, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Attempting to hot reload a modified library from a package ' | 
 |               'marked as non-hot-reloadable: ' | 
 |               "Library: 'package:$packageName/file.dart'", | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |     test('reject reloads when a member is removed.', () async { | 
 |       final initialAndDeltaSource = | 
 |           ''' | 
 |           import 'package:$packageName/file.dart'; | 
 |           main() {} | 
 |           '''; | 
 |       final initialPackageSource = 'class Foo { int member = 100; }'; | 
 |       final deltaPackageSource = 'class Foo {}'; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialAndDeltaSource, | 
 |         initialAndDeltaSource, | 
 |         initialPackageSource: initialPackageSource, | 
 |         deltaPackageSource: deltaPackageSource, | 
 |         packageName: packageName, | 
 |       ); | 
 |       expect( | 
 |         deltaInspector.compareGenerations(initial, delta), | 
 |         unorderedEquals([ | 
 |           'Attempting to hot reload a modified library from a package ' | 
 |               'marked as non-hot-reloadable: ' | 
 |               "Library: 'package:$packageName/file.dart'", | 
 |         ]), | 
 |       ); | 
 |     }); | 
 |     test('accept reloads when introduced but not modified.', () async { | 
 |       final initialSource = ''' | 
 |           main() {} | 
 |           '''; | 
 |       final initialAndDeltaPackageSource = 'class Foo { int member = 100; }'; | 
 |       final deltaSource = | 
 |           ''' | 
 |           import 'package:$packageName/file.dart'; | 
 |           main() {} | 
 |           '''; | 
 |       final (:initial, :delta) = await compileComponents( | 
 |         initialSource, | 
 |         deltaSource, | 
 |         initialPackageSource: initialAndDeltaPackageSource, | 
 |         deltaPackageSource: initialAndDeltaPackageSource, | 
 |         packageName: packageName, | 
 |       ); | 
 |       expect( | 
 |         () => deltaInspector.compareGenerations(initial, delta), | 
 |         returnsNormally, | 
 |       ); | 
 |     }); | 
 |   }); | 
 | } | 
 |  | 
 | /// Test only helper compiles [initialSource] and [deltaSource] and returns two | 
 | /// kernel components. | 
 | /// | 
 | /// Auto-generates a fake package_config.json if [packageName] is provided. | 
 | /// Supports a single package named [packageName] containing a single file | 
 | /// whose source contents across one generation are [initialPackageSource] and | 
 | /// [deltaPackageSource]. | 
 | Future<({Component initial, Component delta})> compileComponents( | 
 |   String initialSource, | 
 |   String deltaSource, { | 
 |   Uri? baseUri, | 
 |   String? packageName, | 
 |   String initialPackageSource = '', | 
 |   String deltaPackageSource = '', | 
 | }) async { | 
 |   baseUri ??= memoryDirectory; | 
 |  | 
 |   final fileName = 'main.dart'; | 
 |   final packageFileName = 'lib/file.dart'; | 
 |   final fileUri = Uri(scheme: baseUri.scheme, host: '', path: fileName); | 
 |   final memoryFileMap = {fileName: initialSource}; | 
 |  | 
 |   // Generate a fake package_config.json and package. | 
 |   Uri? packageConfigUri; | 
 |   if (packageName != null) { | 
 |     packageConfigUri = baseUri.resolve('package_config.json'); | 
 |     memoryFileMap['package_config.json'] = generateFakePackagesFile( | 
 |       packageName: packageName, | 
 |     ); | 
 |     memoryFileMap[packageFileName] = initialPackageSource; | 
 |   } | 
 |   final initialResult = await incrementalComponentFromMemory( | 
 |     memoryFileMap, | 
 |     fileUri, | 
 |     baseUri: baseUri, | 
 |     packageConfigUri: packageConfigUri, | 
 |   ); | 
 |   expect( | 
 |     initialResult.errors, | 
 |     isEmpty, | 
 |     reason: 'Initial source produced compile time errors.', | 
 |   ); | 
 |  | 
 |   memoryFileMap[fileName] = deltaSource; | 
 |   if (packageName != null) { | 
 |     memoryFileMap[packageFileName] = initialPackageSource; | 
 |   } | 
 |   final deltaResult = await incrementalComponentFromMemory( | 
 |     memoryFileMap, | 
 |     fileUri, | 
 |     baseUri: baseUri, | 
 |     packageConfigUri: packageConfigUri, | 
 |     initialCompilerState: initialResult.initialCompilerState, | 
 |   ); | 
 |   expect( | 
 |     deltaResult.errors, | 
 |     isEmpty, | 
 |     reason: 'Delta source produced compile time errors.', | 
 |   ); | 
 |   return ( | 
 |     initial: initialResult.ddcResult.component, | 
 |     delta: deltaResult.ddcResult.component, | 
 |   ); | 
 | } | 
 |  | 
 | String generateFakePackagesFile({ | 
 |   required String packageName, | 
 |   String rootUri = '/', | 
 |   String packageUri = 'lib/', | 
 | }) { | 
 |   return ''' | 
 | { | 
 |   "configVersion": 0, | 
 |   "packages": [ | 
 |     { | 
 |       "name": "$packageName", | 
 |       "rootUri": "$rootUri", | 
 |       "packageUri": "$packageUri", | 
 |       "languageVersion": "3.4" | 
 |     } | 
 |   ] | 
 | } | 
 | '''; | 
 | } |