fix(js_interop_gen): address PR review feedback and add cycle guards - Use cloneType in getLowestCommonAncestorOfTypes to prevent in-place nullability mutation. - Add img, hr, details, summary to allowed HTML tags in doc comment formatting. - Add visited set cycle guards to getMemberHierarchy and findMemberInHierarchy. - Add unit tests in generate_docs_test and type_map_test.
diff --git a/js_interop_gen/lib/src/ast/helpers.dart b/js_interop_gen/lib/src/ast/helpers.dart index 19f8cb9..5021f2c 100644 --- a/js_interop_gen/lib/src/ast/helpers.dart +++ b/js_interop_gen/lib/src/ast/helpers.dart
@@ -83,12 +83,15 @@ Set<String> getMemberHierarchy( Declaration type, [ bool addDirectMembers = false, + Set<Declaration>? visited, ]) { + visited ??= {}; + if (!visited.add(type)) return {}; final members = <String>{}; void addMembersIfReferredType(Type type) { if (type case ReferredType<Declaration>(declaration: final d)) { - members.addAll(getMemberHierarchy(d, true)); + members.addAll(getMemberHierarchy(d, true, visited)); } } @@ -136,7 +139,13 @@ return members; } -MemberDeclaration? findMemberInHierarchy(Declaration td, String name) { +MemberDeclaration? findMemberInHierarchy( + Declaration td, + String name, [ + Set<Declaration>? visited, +]) { + visited ??= {}; + if (!visited.add(td)) return null; if (td is TypeDeclaration) { // Check direct members first final prop = td.properties.where((p) => p.name == name).firstOrNull; @@ -157,7 +166,7 @@ for (final parent in parents) { if (parent case ReferredType(declaration: final d)) { - final found = findMemberInHierarchy(d, name); + final found = findMemberInHierarchy(d, name, visited); if (found != null) return found; } } @@ -178,7 +187,7 @@ // Check constituents recursively for (final parent in td.types) { if (parent case ReferredType(declaration: final d)) { - final found = findMemberInHierarchy(d, name); + final found = findMemberInHierarchy(d, name, visited); if (found != null) return found; } }
diff --git a/js_interop_gen/lib/src/formatting.dart b/js_interop_gen/lib/src/formatting.dart index db31eee..0de8ba9 100644 --- a/js_interop_gen/lib/src/formatting.dart +++ b/js_interop_gen/lib/src/formatting.dart
@@ -24,6 +24,10 @@ 'h5', 'h6', 'blockquote', + 'img', + 'hr', + 'details', + 'summary', '/a', '/b', '/i', @@ -45,6 +49,8 @@ '/h5', '/h6', '/blockquote', + '/details', + '/summary', }; /// Given markdown formatted text [data] and a line [width], return a
diff --git a/js_interop_gen/lib/src/interop_gen/sub_type.dart b/js_interop_gen/lib/src/interop_gen/sub_type.dart index 149d6ab..0eb4974 100644 --- a/js_interop_gen/lib/src/interop_gen/sub_type.dart +++ b/js_interop_gen/lib/src/interop_gen/sub_type.dart
@@ -410,7 +410,7 @@ if (types.isEmpty) throw Exception('You must pass types'); if (types.singleOrNull case final singleType?) { - return singleType..isNullable = isNullable; + return cloneType(singleType, isNullable: isNullable); } if (_getSharedPrimitiveTypeIfAny(types, isNullable: isNullable)
diff --git a/js_interop_gen/test/generate_docs_test.dart b/js_interop_gen/test/generate_docs_test.dart index bd642fb..8557ea0 100644 --- a/js_interop_gen/test/generate_docs_test.dart +++ b/js_interop_gen/test/generate_docs_test.dart
@@ -122,6 +122,15 @@ ''', ); }); + + test('allowed HTML tags (img, hr, details, summary)', () { + compare( + '<img src="test.png"> <hr> <details><summary>Click</summary>Info</details>', + ''' +/// <img src="test.png"> <hr> <details><summary>Click</summary>Info</details> +''', + ); + }); }); }
diff --git a/js_interop_gen/test/type_map_test.dart b/js_interop_gen/test/type_map_test.dart index 0bd5f42..75095c1 100644 --- a/js_interop_gen/test/type_map_test.dart +++ b/js_interop_gen/test/type_map_test.dart
@@ -8,6 +8,7 @@ import 'package:js_interop_gen/src/ast/base.dart'; import 'package:js_interop_gen/src/ast/builtin.dart'; import 'package:js_interop_gen/src/ast/declarations.dart'; +import 'package:js_interop_gen/src/ast/helpers.dart'; import 'package:js_interop_gen/src/ast/types.dart'; import 'package:js_interop_gen/src/interop_gen/namer.dart'; import 'package:js_interop_gen/src/interop_gen/sub_type.dart'; @@ -524,6 +525,33 @@ final hierarchy = getTypeHierarchy(myEnum.asReferredType()); expect(hierarchy.lookup('JSNumber'), isNotNull); }); + + test('Circular type hierarchy cycle guard in helpers', () { + final dummy = InterfaceDeclaration( + name: 'Dummy', + exported: true, + id: ID(type: 'interface', name: 'Dummy'), + ); + late final InterfaceDeclaration a; + late final InterfaceDeclaration b; + + a = InterfaceDeclaration( + name: 'CircA', + exported: true, + id: ID(type: 'interface', name: 'CircA'), + extendedTypes: [ReferredType(name: 'CircB', declaration: dummy)], + ); + b = InterfaceDeclaration( + name: 'CircB', + exported: true, + id: ID(type: 'interface', name: 'CircB'), + extendedTypes: [ReferredType(name: 'CircA', declaration: a)], + ); + (a.extendedTypes.first as ReferredType).declaration = b; + + expect(() => getMemberHierarchy(a), returnsNormally); + expect(() => findMemberInHierarchy(a, 'someProp'), returnsNormally); + }); }); }); }