From 098949bef4f57b96de4eecd0918d9a9dd4bf73dd Mon Sep 17 00:00:00 2001 From: Dong Wook Kim Date: Wed, 9 Aug 2023 12:09:16 +0900 Subject: [PATCH 1/4] [gql_code_builder] optimize slow code generation. (cherry picked from commit fe2e2e2c78cc7263c009fdbc873c9e1eb527c3a8) --- codegen/gql_code_builder/lib/data.dart | 132 ++++++++++++++++++ .../gql_code_builder/lib/src/built_class.dart | 5 +- codegen/gql_code_builder/lib/src/common.dart | 5 +- .../lib/src/inline_fragment_classes.dart | 32 ++++- .../lib/src/operation/data.dart | 107 +++++++++++--- .../lib/src/when_extension.dart | 9 +- 6 files changed, 259 insertions(+), 31 deletions(-) diff --git a/codegen/gql_code_builder/lib/data.dart b/codegen/gql_code_builder/lib/data.dart index b64d6607..58029d7e 100644 --- a/codegen/gql_code_builder/lib/data.dart +++ b/codegen/gql_code_builder/lib/data.dart @@ -1,6 +1,7 @@ import "package:built_collection/built_collection.dart"; import "package:code_builder/code_builder.dart"; import "package:gql/ast.dart"; +import "package:gql_code_builder/src/common.dart"; import "package:gql_code_builder/src/config/when_extension_config.dart"; import "./source.dart"; @@ -19,6 +20,9 @@ Library buildDataLibrary( generateMaybeWhenExtensionMethod: false, ), ]) { + final fragmentMap = _fragmentMap(docSource); + final dataClassAliasMap = _dataClassAliasMap(docSource, fragmentMap); + final operationDataClasses = docSource.document.definitions .whereType() .expand( @@ -28,6 +32,8 @@ Library buildDataLibrary( schemaSource, typeOverrides, whenExtensionConfig, + fragmentMap, + dataClassAliasMap, ), ) .toList(); @@ -41,6 +47,8 @@ Library buildDataLibrary( schemaSource, typeOverrides, whenExtensionConfig, + fragmentMap, + dataClassAliasMap, ), ) .toList(); @@ -54,3 +62,127 @@ Library buildDataLibrary( ]), ); } + + +Map _fragmentMap(SourceNode source) => { + for (var def + in source.document.definitions.whereType()) + def.name.value: SourceSelections( + url: source.url, + selections: def.selectionSet.selections, + ), + for (var import in source.imports) ..._fragmentMap(import) +}; + +Map _dataClassAliasMap(SourceNode source, Map fragmentMap, [Map? aliasMap, Set? visitedSource]) { + aliasMap ??= {}; + visitedSource ??= {}; + + source.imports.forEach((s) { + if (!visitedSource!.contains(source.url)) { + visitedSource.add(source.url); + _dataClassAliasMap(s, fragmentMap, aliasMap); + } + }); + + for (final def in source.document.definitions.whereType()) { + _dataClassAliasMapDFS( + typeRefPrefix: builtClassName("${def.name!.value}Data"), + getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data", + selections: def.selectionSet.selections, + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + } + + for (final def in source.document.definitions.whereType()) { + _dataClassAliasMapDFS( + typeRefPrefix: builtClassName(def.name.value), + getAliasTypeName: builtClassName, + selections: def.selectionSet.selections, + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + _dataClassAliasMapDFS( + typeRefPrefix: builtClassName("${def.name.value}Data"), + getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data", + selections: def.selectionSet.selections, + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + } + + return aliasMap; +} + +void _dataClassAliasMapDFS({ + required String typeRefPrefix, + required String Function(String fragmentName) getAliasTypeName, + required List selections, + required Map fragmentMap, + required Map aliasMap, +}) { + if (selections.isEmpty) return; + + // flatten selections to extract untouched fragments while visiting children. + final shrunkenSelections = shrinkSelections(mergeSelections(selections, fragmentMap), fragmentMap); + + // alias single fragment and finish + final selectionsWithoutTypename = shrunkenSelections.where((s) => !(s is FieldNode && s.name.value == "__typename")); + if (selectionsWithoutTypename.length == 1 && selectionsWithoutTypename.first is FragmentSpreadNode) { + final node = selectionsWithoutTypename.first as FragmentSpreadNode; + final fragment = fragmentMap[node.name.value]; + final fragmentTypeName = getAliasTypeName(node.name.value); + aliasMap[typeRefPrefix] = refer(fragmentTypeName, "${fragment!.url ?? ""}#data"); + // print("alias $typeRefPrefix => $fragmentTypeName"); + return; + } + + for (final node in selectionsWithoutTypename) { + if (node is FragmentSpreadNode) { + // exclude redefined selections from each fragment selections + final fragmentSelections = fragmentMap[node.name.value]!.selections; + final exclusiveFragmentSelections = mergeSelections(fragmentSelections, fragmentMap).where((s1) { + if (s1 is FieldNode) { + final name = (s1.alias ?? s1.name).value; + return selectionsWithoutTypename.whereType().every((s2) => name != (s2.alias ?? s2.name).value); + } else if (s1 is InlineFragmentNode && s1.typeCondition != null) { + /// TODO: Handle inline fragments without a type condition + final name = s1.typeCondition!.on.name.value; + return selectionsWithoutTypename.whereType().every((s2) => name != s2.typeCondition?.on.name.value); + } + return false; + }).toList(); + + _dataClassAliasMapDFS( + typeRefPrefix: typeRefPrefix, + getAliasTypeName: getAliasTypeName, + selections: exclusiveFragmentSelections, + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + } else if (node is InlineFragmentNode) { + if (node.typeCondition != null) { + /// TODO: Handle inline fragments without a type condition + _dataClassAliasMapDFS( + typeRefPrefix: "${typeRefPrefix}__as${node.typeCondition!.on.name.value}", + getAliasTypeName: getAliasTypeName, + selections: [ + ...selections.where((s) => !(s is InlineFragmentNode)), + ...node.selectionSet.selections, + ], + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + } + } else if (node is FieldNode && node.selectionSet != null) { + _dataClassAliasMapDFS( + typeRefPrefix: "${typeRefPrefix}_${(node.alias ?? node.name).value}", + getAliasTypeName: getAliasTypeName, + selections: node.selectionSet!.selections, + fragmentMap: fragmentMap, + aliasMap: aliasMap, + ); + } + } +} \ No newline at end of file diff --git a/codegen/gql_code_builder/lib/src/built_class.dart b/codegen/gql_code_builder/lib/src/built_class.dart index 631ba97b..bdf6439c 100644 --- a/codegen/gql_code_builder/lib/src/built_class.dart +++ b/codegen/gql_code_builder/lib/src/built_class.dart @@ -11,6 +11,7 @@ Class builtClass({ Map? initializers, Map superclassSelections = const {}, List methods = const [], + Map? dataClassAliasMap, }) { final className = builtClassName(name); return Class( @@ -30,7 +31,9 @@ Class builtClass({ ], ), ), - ...superclassSelections.keys.map( + ...superclassSelections.keys + .where((superName) => dataClassAliasMap?.containsKey(builtClassName(superName)) != true) + .map( (superName) => refer( builtClassName(superName), (superclassSelections[superName]?.url ?? "") + "#data", diff --git a/codegen/gql_code_builder/lib/src/common.dart b/codegen/gql_code_builder/lib/src/common.dart index 7cbcd01a..590a575e 100644 --- a/codegen/gql_code_builder/lib/src/common.dart +++ b/codegen/gql_code_builder/lib/src/common.dart @@ -138,6 +138,7 @@ Method buildGetter({ required TypeNode typeNode, required SourceNode schemaSource, Map typeOverrides = const {}, + Reference? typeRefAlias, String? typeRefPrefix, bool built = true, bool isOverride = false, @@ -151,7 +152,9 @@ Method buildGetter({ final typeMap = { ...defaultTypeMap, - if (typeRefPrefix != null) + if (typeRefAlias != null) + typeName: typeRefAlias + else if (typeRefPrefix != null) typeName: refer("${typeRefPrefix}_${nameNode.value}") else if (typeDef != null) typeName: refer( diff --git a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart index 1eff2d74..3f350b5c 100644 --- a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart +++ b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart @@ -22,6 +22,7 @@ List buildInlineFragmentClasses({ required String type, required Map typeOverrides, required Map fragmentMap, + required Map dataClassAliasMap, required Map superclassSelections, required List inlineFragments, required bool built, @@ -31,6 +32,7 @@ List buildInlineFragmentClasses({ baseTypeName: name, inlineFragments: inlineFragments, config: whenExtensionConfig, + dataClassAliasMap: dataClassAliasMap, ); return [ Class( @@ -38,12 +40,14 @@ List buildInlineFragmentClasses({ ..abstract = true ..name = builtClassName(name) ..implements.addAll( - superclassSelections.keys.map( - (superName) => refer( - builtClassName(superName), - (superclassSelections[superName]?.url ?? "") + "#data", + superclassSelections.keys + .where((superName) => !dataClassAliasMap.containsKey(builtClassName(superName))) + .map( + (superName) => refer( + builtClassName(superName), + (superclassSelections[superName]?.url ?? "") + "#data", + ), ), - ), ) ..methods.addAll([ ...fieldGetters, @@ -51,6 +55,7 @@ List buildInlineFragmentClasses({ ..._inlineFragmentRootSerializationMethods( name: builtClassName(name), inlineFragments: inlineFragments, + dataClassAliasMap: dataClassAliasMap, ), ]), ), @@ -65,6 +70,7 @@ List buildInlineFragmentClasses({ fragmentMap, ), fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, schemaSource: schemaSource, type: type, typeOverrides: typeOverrides, @@ -77,7 +83,17 @@ List buildInlineFragmentClasses({ /// TODO: Handle inline fragments without a type condition /// https://spec.graphql.org/June2018/#sec-Inline-Fragments - ...inlineFragments.where((frag) => frag.typeCondition != null).expand( + ...inlineFragments.where((frag) { + if (frag.typeCondition == null) { + return false; + } + final typeName = builtClassName("${name}__as${frag.typeCondition!.on.name.value}"); + if (dataClassAliasMap.containsKey(typeName)) { + // print("alias $typeName => ${dataClassAliasMap[typeName]!.symbol}"); + return false; + } + return true; + }).expand( (inlineFragment) => buildSelectionSetDataClasses( name: "${name}__as${inlineFragment.typeCondition!.on.name.value}", selections: mergeSelections( @@ -89,6 +105,7 @@ List buildInlineFragmentClasses({ fragmentMap, ), fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, schemaSource: schemaSource, type: inlineFragment.typeCondition!.on.name.value, typeOverrides: typeOverrides, @@ -104,6 +121,7 @@ List buildInlineFragmentClasses({ List _inlineFragmentRootSerializationMethods({ required String name, required List inlineFragments, + required Map dataClassAliasMap, }) => [ buildSerializerGetter(name).rebuild( @@ -121,7 +139,7 @@ List _inlineFragmentRootSerializationMethods({ { for (var v in inlineFragments .where((frag) => frag.typeCondition != null)) - "${v.typeCondition!.on.name.value}": refer( + "${v.typeCondition!.on.name.value}": dataClassAliasMap["${name}__as${v.typeCondition!.on.name.value}"] ?? refer( "${name}__as${v.typeCondition!.on.name.value}", ) }, diff --git a/codegen/gql_code_builder/lib/src/operation/data.dart b/codegen/gql_code_builder/lib/src/operation/data.dart index 7a43b669..a68b4f7f 100644 --- a/codegen/gql_code_builder/lib/src/operation/data.dart +++ b/codegen/gql_code_builder/lib/src/operation/data.dart @@ -1,4 +1,5 @@ import "package:code_builder/code_builder.dart"; +import "package:collection/collection.dart"; import "package:gql/ast.dart"; import "package:gql_code_builder/src/config/when_extension_config.dart"; @@ -13,12 +14,13 @@ List buildOperationDataClasses( SourceNode schemaSource, Map typeOverrides, InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig, + Map fragmentMap, + Map dataClassAliasMap, ) { if (op.name == null) { throw Exception("Operations must be named"); } - final fragmentMap = _fragmentMap(docSource); return buildSelectionSetDataClasses( name: "${op.name!.value}Data", selections: mergeSelections( @@ -32,6 +34,7 @@ List buildOperationDataClasses( ), typeOverrides: typeOverrides, fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, superclassSelections: {}, whenExtensionConfig: whenExtensionConfig, ); @@ -43,8 +46,9 @@ List buildFragmentDataClasses( SourceNode schemaSource, Map typeOverrides, InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig, + Map fragmentMap, + Map dataClassAliasMap, ) { - final fragmentMap = _fragmentMap(docSource); final selections = mergeSelections( frag.selectionSet.selections, fragmentMap, @@ -58,6 +62,7 @@ List buildFragmentDataClasses( type: frag.typeCondition.on.name.value, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, superclassSelections: {}, built: false, whenExtensionConfig: whenExtensionConfig, @@ -70,6 +75,7 @@ List buildFragmentDataClasses( type: frag.typeCondition.on.name.value, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, superclassSelections: { frag.name.value: SourceSelections( url: docSource.url, @@ -98,16 +104,6 @@ String _operationType( .value; } -Map _fragmentMap(SourceNode source) => { - for (var def - in source.document.definitions.whereType()) - def.name.value: SourceSelections( - url: source.url, - selections: def.selectionSet.selections, - ), - for (var import in source.imports) ..._fragmentMap(import) - }; - /// Builds one or more data classes, with properties based on [selections]. /// /// For each selection that is a field with nested selections, a descendent @@ -124,6 +120,7 @@ List buildSelectionSetDataClasses({ required String type, required Map typeOverrides, required Map fragmentMap, + required Map dataClassAliasMap, required Map superclassSelections, bool built = true, required InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig, @@ -162,6 +159,7 @@ List buildSelectionSetDataClasses({ typeNode: typeNode, schemaSource: schemaSource, typeOverrides: typeOverrides, + typeRefAlias: dataClassAliasMap[builtClassName("${name}_${nameNode.value}")], typeRefPrefix: node.selectionSet != null ? builtClassName(name) : null, built: built, isOverride: superclassSelectionNodes.contains(node), @@ -181,18 +179,21 @@ List buildSelectionSetDataClasses({ type: type, typeOverrides: typeOverrides, fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, superclassSelections: superclassSelections, inlineFragments: inlineFragments, built: built, whenExtensionConfig: whenExtensionConfig, ) - else if (!built) + else if (!built && dataClassAliasMap[name] == null) Class( (b) => b ..abstract = true ..name = builtClassName(name) ..implements.addAll( - superclassSelections.keys.map( + superclassSelections.keys + .where((superName) => !dataClassAliasMap.containsKey(builtClassName(superName))) + .map( (superName) => refer( builtClassName(superName), (superclassSelections[superName]?.url ?? "") + "#data", @@ -217,18 +218,21 @@ List buildSelectionSetDataClasses({ "G__typename": literalString(type), }, superclassSelections: superclassSelections, + dataClassAliasMap: dataClassAliasMap, ), // Build classes for each field that includes selections ...selections .whereType() .where( - (field) => field.selectionSet != null, + (field) => field.selectionSet != null + && !dataClassAliasMap.containsKey(builtClassName("${name}_${field.alias?.value ?? field.name.value}")), ) .expand( (field) => buildSelectionSetDataClasses( name: "${name}_${field.alias?.value ?? field.name.value}", selections: field.selectionSet!.selections, fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, schemaSource: schemaSource, type: unwrapTypeNode( _getFieldTypeNode( @@ -251,6 +255,51 @@ List buildSelectionSetDataClasses({ ]; } +/// Shrink merged fields nodes based on FragmentMap +List shrinkSelections( + List selections, + Map fragmentMap, + ) { + final unmerged = [...selections]; + + for (final selection in selections) { + if (selection is FieldNode && selection.selectionSet != null) { + final index = unmerged.indexOf(selection); + unmerged[index] = FieldNode( + name: selection.name, + alias: selection.alias, + selectionSet: SelectionSetNode( + selections: shrinkSelections(selection.selectionSet!.selections, fragmentMap), + ), + ); + } else if (selection is InlineFragmentNode && selection.typeCondition != null) { + /// TODO: Handle inline fragments without a type condition + final index = unmerged.indexOf(selection); + unmerged[index] = InlineFragmentNode( + typeCondition: selection.typeCondition, + directives: selection.directives, + selectionSet: SelectionSetNode( + selections: shrinkSelections(selection.selectionSet.selections, fragmentMap), + ), + ); + } + } + + for (final node in unmerged.whereType().toList()) { + final fragment = fragmentMap[node.name.value]!; + final spreadIndex = unmerged.indexOf(node); + final duplicateIndexList = []; + unmerged.forEachIndexed((selectionIndex, selection) { + if (selectionIndex > spreadIndex && fragment.selections.any((s) => s.hashCode == selection.hashCode)) { + duplicateIndexList.add(selectionIndex); + } + }); + duplicateIndexList.reversed.forEach(unmerged.removeAt); + } + + return unmerged; +} + /// Deeply merges field nodes List mergeSelections( List selections, @@ -267,7 +316,7 @@ List mergeSelections( } else { final existingNode = selectionMap[key]; final existingSelections = existingNode is FieldNode && - existingNode.selectionSet != null + existingNode.selectionSet != null ? existingNode.selectionSet!.selections : []; selectionMap[key] = FieldNode( @@ -275,12 +324,32 @@ List mergeSelections( alias: selection.alias, selectionSet: SelectionSetNode( selections: mergeSelections( + [ + ...existingSelections, + ...selection.selectionSet!.selections + ], + fragmentMap, + ))); + } + } else if (selection is InlineFragmentNode && selection.typeCondition != null) { + /// TODO: Handle inline fragments without a type condition + final key = selection.typeCondition!.on.name.value; + if (selectionMap.containsKey(key)) { + selectionMap[key] = InlineFragmentNode( + typeCondition: selection.typeCondition, + directives: selection.directives, + selectionSet: SelectionSetNode( + selections: mergeSelections( [ - ...existingSelections, - ...selection.selectionSet!.selections + ...(selectionMap[key] as InlineFragmentNode).selectionSet.selections, + ...selection.selectionSet.selections, ], fragmentMap, - ))); + ), + ), + ); + } else { + selectionMap[key] = selection; } } else { selectionMap[selection.hashCode.toString()] = selection; diff --git a/codegen/gql_code_builder/lib/src/when_extension.dart b/codegen/gql_code_builder/lib/src/when_extension.dart index 52987e54..4616601a 100644 --- a/codegen/gql_code_builder/lib/src/when_extension.dart +++ b/codegen/gql_code_builder/lib/src/when_extension.dart @@ -39,7 +39,8 @@ Code _caseStatement(InlineFragmentNode inlineFragment) => Extension? inlineFragmentWhenExtension( {required String baseTypeName, required List inlineFragments, - required InlineFragmentSpreadWhenExtensionConfig config}) { + required InlineFragmentSpreadWhenExtensionConfig config, + required Map dataClassAliasMap}) { final inlineFragmentsWithTypConditions = inlineFragments .where((inlineFragment) => inlineFragment.typeCondition != null) .toList(); @@ -54,8 +55,10 @@ Extension? inlineFragmentWhenExtension( /// returns the name of the concrete built class for the inlineFragment /// so we can refer to it in the generated code - String getGeneratedTypeName(InlineFragmentNode node) => - builtClassName("${baseTypeName}__as${node.typeCondition!.on.name.value}"); + String getGeneratedTypeName(InlineFragmentNode node) { + final typeName = builtClassName("${baseTypeName}__as${node.typeCondition!.on.name.value}"); + return dataClassAliasMap[typeName]?.symbol ?? typeName; + } /// a pool of parameter names which have already been used /// so we can avoid name clashes From bad14f7675643ffef0ffeff9abef3ed3c4f82ba7 Mon Sep 17 00:00:00 2001 From: Dong Wook Kim Date: Thu, 10 Aug 2023 00:39:01 +0900 Subject: [PATCH 2/4] reformat codes --- codegen/gql_code_builder/lib/data.dart | 55 ++++++++++------ .../gql_code_builder/lib/src/built_class.dart | 14 ++-- .../lib/src/inline_fragment_classes.dart | 66 ++++++++++--------- .../lib/src/operation/data.dart | 61 ++++++++++------- .../lib/src/when_extension.dart | 3 +- 5 files changed, 115 insertions(+), 84 deletions(-) diff --git a/codegen/gql_code_builder/lib/data.dart b/codegen/gql_code_builder/lib/data.dart index 58029d7e..9bbf6dac 100644 --- a/codegen/gql_code_builder/lib/data.dart +++ b/codegen/gql_code_builder/lib/data.dart @@ -63,18 +63,19 @@ Library buildDataLibrary( ); } - Map _fragmentMap(SourceNode source) => { - for (var def - in source.document.definitions.whereType()) - def.name.value: SourceSelections( - url: source.url, - selections: def.selectionSet.selections, - ), - for (var import in source.imports) ..._fragmentMap(import) -}; + for (var def + in source.document.definitions.whereType()) + def.name.value: SourceSelections( + url: source.url, + selections: def.selectionSet.selections, + ), + for (var import in source.imports) ..._fragmentMap(import) + }; -Map _dataClassAliasMap(SourceNode source, Map fragmentMap, [Map? aliasMap, Set? visitedSource]) { +Map _dataClassAliasMap( + SourceNode source, Map fragmentMap, + [Map? aliasMap, Set? visitedSource]) { aliasMap ??= {}; visitedSource ??= {}; @@ -85,7 +86,8 @@ Map _dataClassAliasMap(SourceNode source, Map()) { + for (final def + in source.document.definitions.whereType()) { _dataClassAliasMapDFS( typeRefPrefix: builtClassName("${def.name!.value}Data"), getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data", @@ -95,7 +97,8 @@ Map _dataClassAliasMap(SourceNode source, Map()) { + for (final def + in source.document.definitions.whereType()) { _dataClassAliasMapDFS( typeRefPrefix: builtClassName(def.name.value), getAliasTypeName: builtClassName, @@ -125,15 +128,19 @@ void _dataClassAliasMapDFS({ if (selections.isEmpty) return; // flatten selections to extract untouched fragments while visiting children. - final shrunkenSelections = shrinkSelections(mergeSelections(selections, fragmentMap), fragmentMap); + final shrunkenSelections = + shrinkSelections(mergeSelections(selections, fragmentMap), fragmentMap); // alias single fragment and finish - final selectionsWithoutTypename = shrunkenSelections.where((s) => !(s is FieldNode && s.name.value == "__typename")); - if (selectionsWithoutTypename.length == 1 && selectionsWithoutTypename.first is FragmentSpreadNode) { + final selectionsWithoutTypename = shrunkenSelections + .where((s) => !(s is FieldNode && s.name.value == "__typename")); + if (selectionsWithoutTypename.length == 1 && + selectionsWithoutTypename.first is FragmentSpreadNode) { final node = selectionsWithoutTypename.first as FragmentSpreadNode; final fragment = fragmentMap[node.name.value]; final fragmentTypeName = getAliasTypeName(node.name.value); - aliasMap[typeRefPrefix] = refer(fragmentTypeName, "${fragment!.url ?? ""}#data"); + aliasMap[typeRefPrefix] = + refer(fragmentTypeName, "${fragment!.url ?? ""}#data"); // print("alias $typeRefPrefix => $fragmentTypeName"); return; } @@ -142,14 +149,19 @@ void _dataClassAliasMapDFS({ if (node is FragmentSpreadNode) { // exclude redefined selections from each fragment selections final fragmentSelections = fragmentMap[node.name.value]!.selections; - final exclusiveFragmentSelections = mergeSelections(fragmentSelections, fragmentMap).where((s1) { + final exclusiveFragmentSelections = + mergeSelections(fragmentSelections, fragmentMap).where((s1) { if (s1 is FieldNode) { final name = (s1.alias ?? s1.name).value; - return selectionsWithoutTypename.whereType().every((s2) => name != (s2.alias ?? s2.name).value); + return selectionsWithoutTypename + .whereType() + .every((s2) => name != (s2.alias ?? s2.name).value); } else if (s1 is InlineFragmentNode && s1.typeCondition != null) { /// TODO: Handle inline fragments without a type condition final name = s1.typeCondition!.on.name.value; - return selectionsWithoutTypename.whereType().every((s2) => name != s2.typeCondition?.on.name.value); + return selectionsWithoutTypename + .whereType() + .every((s2) => name != s2.typeCondition?.on.name.value); } return false; }).toList(); @@ -165,7 +177,8 @@ void _dataClassAliasMapDFS({ if (node.typeCondition != null) { /// TODO: Handle inline fragments without a type condition _dataClassAliasMapDFS( - typeRefPrefix: "${typeRefPrefix}__as${node.typeCondition!.on.name.value}", + typeRefPrefix: + "${typeRefPrefix}__as${node.typeCondition!.on.name.value}", getAliasTypeName: getAliasTypeName, selections: [ ...selections.where((s) => !(s is InlineFragmentNode)), @@ -185,4 +198,4 @@ void _dataClassAliasMapDFS({ ); } } -} \ No newline at end of file +} diff --git a/codegen/gql_code_builder/lib/src/built_class.dart b/codegen/gql_code_builder/lib/src/built_class.dart index bdf6439c..135507ae 100644 --- a/codegen/gql_code_builder/lib/src/built_class.dart +++ b/codegen/gql_code_builder/lib/src/built_class.dart @@ -32,13 +32,15 @@ Class builtClass({ ), ), ...superclassSelections.keys - .where((superName) => dataClassAliasMap?.containsKey(builtClassName(superName)) != true) + .where((superName) => + dataClassAliasMap?.containsKey(builtClassName(superName)) != + true) .map( - (superName) => refer( - builtClassName(superName), - (superclassSelections[superName]?.url ?? "") + "#data", - ), - ) + (superName) => refer( + builtClassName(superName), + (superclassSelections[superName]?.url ?? "") + "#data", + ), + ) ], ) ..constructors.addAll( diff --git a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart index 3f350b5c..7ccddb9c 100644 --- a/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart +++ b/codegen/gql_code_builder/lib/src/inline_fragment_classes.dart @@ -41,13 +41,14 @@ List buildInlineFragmentClasses({ ..name = builtClassName(name) ..implements.addAll( superclassSelections.keys - .where((superName) => !dataClassAliasMap.containsKey(builtClassName(superName))) - .map( - (superName) => refer( - builtClassName(superName), - (superclassSelections[superName]?.url ?? "") + "#data", + .where((superName) => + !dataClassAliasMap.containsKey(builtClassName(superName))) + .map( + (superName) => refer( + builtClassName(superName), + (superclassSelections[superName]?.url ?? "") + "#data", + ), ), - ), ) ..methods.addAll([ ...fieldGetters, @@ -87,34 +88,35 @@ List buildInlineFragmentClasses({ if (frag.typeCondition == null) { return false; } - final typeName = builtClassName("${name}__as${frag.typeCondition!.on.name.value}"); + final typeName = + builtClassName("${name}__as${frag.typeCondition!.on.name.value}"); if (dataClassAliasMap.containsKey(typeName)) { // print("alias $typeName => ${dataClassAliasMap[typeName]!.symbol}"); return false; } return true; }).expand( - (inlineFragment) => buildSelectionSetDataClasses( - name: "${name}__as${inlineFragment.typeCondition!.on.name.value}", - selections: mergeSelections( - [ - ...selections.whereType(), - ...selections.whereType(), - ...inlineFragment.selectionSet.selections, - ], - fragmentMap, - ), - fragmentMap: fragmentMap, - dataClassAliasMap: dataClassAliasMap, - schemaSource: schemaSource, - type: inlineFragment.typeCondition!.on.name.value, - typeOverrides: typeOverrides, - superclassSelections: { - name: SourceSelections(url: null, selections: selections) - }, - built: built, - whenExtensionConfig: whenExtensionConfig), - ), + (inlineFragment) => buildSelectionSetDataClasses( + name: "${name}__as${inlineFragment.typeCondition!.on.name.value}", + selections: mergeSelections( + [ + ...selections.whereType(), + ...selections.whereType(), + ...inlineFragment.selectionSet.selections, + ], + fragmentMap, + ), + fragmentMap: fragmentMap, + dataClassAliasMap: dataClassAliasMap, + schemaSource: schemaSource, + type: inlineFragment.typeCondition!.on.name.value, + typeOverrides: typeOverrides, + superclassSelections: { + name: SourceSelections(url: null, selections: selections) + }, + built: built, + whenExtensionConfig: whenExtensionConfig), + ), ]; } @@ -139,9 +141,11 @@ List _inlineFragmentRootSerializationMethods({ { for (var v in inlineFragments .where((frag) => frag.typeCondition != null)) - "${v.typeCondition!.on.name.value}": dataClassAliasMap["${name}__as${v.typeCondition!.on.name.value}"] ?? refer( - "${name}__as${v.typeCondition!.on.name.value}", - ) + "${v.typeCondition!.on.name.value}": dataClassAliasMap[ + "${name}__as${v.typeCondition!.on.name.value}"] ?? + refer( + "${name}__as${v.typeCondition!.on.name.value}", + ) }, ), ]).code, diff --git a/codegen/gql_code_builder/lib/src/operation/data.dart b/codegen/gql_code_builder/lib/src/operation/data.dart index a68b4f7f..e8fea5ab 100644 --- a/codegen/gql_code_builder/lib/src/operation/data.dart +++ b/codegen/gql_code_builder/lib/src/operation/data.dart @@ -159,7 +159,8 @@ List buildSelectionSetDataClasses({ typeNode: typeNode, schemaSource: schemaSource, typeOverrides: typeOverrides, - typeRefAlias: dataClassAliasMap[builtClassName("${name}_${nameNode.value}")], + typeRefAlias: + dataClassAliasMap[builtClassName("${name}_${nameNode.value}")], typeRefPrefix: node.selectionSet != null ? builtClassName(name) : null, built: built, isOverride: superclassSelectionNodes.contains(node), @@ -192,13 +193,14 @@ List buildSelectionSetDataClasses({ ..name = builtClassName(name) ..implements.addAll( superclassSelections.keys - .where((superName) => !dataClassAliasMap.containsKey(builtClassName(superName))) + .where((superName) => + !dataClassAliasMap.containsKey(builtClassName(superName))) .map( - (superName) => refer( - builtClassName(superName), - (superclassSelections[superName]?.url ?? "") + "#data", - ), - ), + (superName) => refer( + builtClassName(superName), + (superclassSelections[superName]?.url ?? "") + "#data", + ), + ), ) ..methods.addAll([ ...fieldGetters, @@ -224,8 +226,10 @@ List buildSelectionSetDataClasses({ ...selections .whereType() .where( - (field) => field.selectionSet != null - && !dataClassAliasMap.containsKey(builtClassName("${name}_${field.alias?.value ?? field.name.value}")), + (field) => + field.selectionSet != null && + !dataClassAliasMap.containsKey(builtClassName( + "${name}_${field.alias?.value ?? field.name.value}")), ) .expand( (field) => buildSelectionSetDataClasses( @@ -257,9 +261,9 @@ List buildSelectionSetDataClasses({ /// Shrink merged fields nodes based on FragmentMap List shrinkSelections( - List selections, - Map fragmentMap, - ) { + List selections, + Map fragmentMap, +) { final unmerged = [...selections]; for (final selection in selections) { @@ -269,17 +273,20 @@ List shrinkSelections( name: selection.name, alias: selection.alias, selectionSet: SelectionSetNode( - selections: shrinkSelections(selection.selectionSet!.selections, fragmentMap), + selections: + shrinkSelections(selection.selectionSet!.selections, fragmentMap), ), ); - } else if (selection is InlineFragmentNode && selection.typeCondition != null) { + } else if (selection is InlineFragmentNode && + selection.typeCondition != null) { /// TODO: Handle inline fragments without a type condition final index = unmerged.indexOf(selection); unmerged[index] = InlineFragmentNode( typeCondition: selection.typeCondition, directives: selection.directives, selectionSet: SelectionSetNode( - selections: shrinkSelections(selection.selectionSet.selections, fragmentMap), + selections: + shrinkSelections(selection.selectionSet.selections, fragmentMap), ), ); } @@ -290,7 +297,8 @@ List shrinkSelections( final spreadIndex = unmerged.indexOf(node); final duplicateIndexList = []; unmerged.forEachIndexed((selectionIndex, selection) { - if (selectionIndex > spreadIndex && fragment.selections.any((s) => s.hashCode == selection.hashCode)) { + if (selectionIndex > spreadIndex && + fragment.selections.any((s) => s.hashCode == selection.hashCode)) { duplicateIndexList.add(selectionIndex); } }); @@ -316,7 +324,7 @@ List mergeSelections( } else { final existingNode = selectionMap[key]; final existingSelections = existingNode is FieldNode && - existingNode.selectionSet != null + existingNode.selectionSet != null ? existingNode.selectionSet!.selections : []; selectionMap[key] = FieldNode( @@ -324,14 +332,15 @@ List mergeSelections( alias: selection.alias, selectionSet: SelectionSetNode( selections: mergeSelections( - [ - ...existingSelections, - ...selection.selectionSet!.selections - ], - fragmentMap, - ))); + [ + ...existingSelections, + ...selection.selectionSet!.selections + ], + fragmentMap, + ))); } - } else if (selection is InlineFragmentNode && selection.typeCondition != null) { + } else if (selection is InlineFragmentNode && + selection.typeCondition != null) { /// TODO: Handle inline fragments without a type condition final key = selection.typeCondition!.on.name.value; if (selectionMap.containsKey(key)) { @@ -341,7 +350,9 @@ List mergeSelections( selectionSet: SelectionSetNode( selections: mergeSelections( [ - ...(selectionMap[key] as InlineFragmentNode).selectionSet.selections, + ...(selectionMap[key] as InlineFragmentNode) + .selectionSet + .selections, ...selection.selectionSet.selections, ], fragmentMap, diff --git a/codegen/gql_code_builder/lib/src/when_extension.dart b/codegen/gql_code_builder/lib/src/when_extension.dart index 4616601a..197bcab4 100644 --- a/codegen/gql_code_builder/lib/src/when_extension.dart +++ b/codegen/gql_code_builder/lib/src/when_extension.dart @@ -56,7 +56,8 @@ Extension? inlineFragmentWhenExtension( /// returns the name of the concrete built class for the inlineFragment /// so we can refer to it in the generated code String getGeneratedTypeName(InlineFragmentNode node) { - final typeName = builtClassName("${baseTypeName}__as${node.typeCondition!.on.name.value}"); + final typeName = builtClassName( + "${baseTypeName}__as${node.typeCondition!.on.name.value}"); return dataClassAliasMap[typeName]?.symbol ?? typeName; } From d1ccfc499045005bdb4cdf7327ae28a2fd0d15a7 Mon Sep 17 00:00:00 2001 From: Dong Wook Kim Date: Mon, 14 Aug 2023 00:01:15 +0900 Subject: [PATCH 3/4] fix multiple inlineFragments selection not generating subclasses issue --- codegen/gql_code_builder/lib/data.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codegen/gql_code_builder/lib/data.dart b/codegen/gql_code_builder/lib/data.dart index 9bbf6dac..44193975 100644 --- a/codegen/gql_code_builder/lib/data.dart +++ b/codegen/gql_code_builder/lib/data.dart @@ -181,7 +181,7 @@ void _dataClassAliasMapDFS({ "${typeRefPrefix}__as${node.typeCondition!.on.name.value}", getAliasTypeName: getAliasTypeName, selections: [ - ...selections.where((s) => !(s is InlineFragmentNode)), + ...selections.where((s) => s != node), ...node.selectionSet.selections, ], fragmentMap: fragmentMap, From 4f55b56646e52c56c24a4873f55d36d44208f254 Mon Sep 17 00:00:00 2001 From: Dong Wook Kim Date: Mon, 14 Aug 2023 00:39:16 +0900 Subject: [PATCH 4/4] add dataClassConfig to deliver reuse_fragments optional feature --- codegen/gql_build/lib/gql_build.dart | 14 ++++++++------ codegen/gql_build/lib/src/data_builder.dart | 16 +++++++++++----- codegen/gql_build/lib/src/utils/config.dart | 4 ++++ codegen/gql_code_builder/lib/data.dart | 9 ++++++++- .../lib/src/config/data_class_config.dart | 8 ++++++++ 5 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 codegen/gql_code_builder/lib/src/config/data_class_config.dart diff --git a/codegen/gql_build/lib/gql_build.dart b/codegen/gql_build/lib/gql_build.dart index 718bee0f..9e8efd8c 100644 --- a/codegen/gql_build/lib/gql_build.dart +++ b/codegen/gql_build/lib/gql_build.dart @@ -19,12 +19,14 @@ Builder dataBuilder( BuilderOptions options, ) => DataBuilder( - AssetId.parse( - options.config["schema"] as String, - ), - (options.config["add_typenames"] ?? true) as bool, - typeOverrideMap(options.config["type_overrides"]), - whenExtensionConfig: whenExtensionConfig(options.config)); + AssetId.parse( + options.config["schema"] as String, + ), + (options.config["add_typenames"] ?? true) as bool, + typeOverrideMap(options.config["type_overrides"]), + whenExtensionConfig: whenExtensionConfig(options.config), + dataClassConfig: dataClassConfig(options.config), + ); /// Builds GraphQL type-safe request builder Builder reqBuilder( diff --git a/codegen/gql_build/lib/src/data_builder.dart b/codegen/gql_build/lib/src/data_builder.dart index 641c17a3..f81b0fff 100644 --- a/codegen/gql_build/lib/src/data_builder.dart +++ b/codegen/gql_build/lib/src/data_builder.dart @@ -14,6 +14,7 @@ class DataBuilder implements Builder { final bool addTypenames; final Map typeOverrides; final InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig; + final DataClassConfig dataClassConfig; DataBuilder( this.schemaId, @@ -23,6 +24,9 @@ class DataBuilder implements Builder { generateWhenExtensionMethod: false, generateMaybeWhenExtensionMethod: false, ), + this.dataClassConfig = const DataClassConfig( + reuseFragments: false, + ), }); @override @@ -41,11 +45,13 @@ class DataBuilder implements Builder { .path; final library = buildDataLibrary( - addTypenames ? introspection.addTypenames(doc) : doc, - introspection.addTypenames(schema), - basename(generatedPartUrl), - typeOverrides, - whenExtensionConfig); + addTypenames ? introspection.addTypenames(doc) : doc, + introspection.addTypenames(schema), + basename(generatedPartUrl), + typeOverrides, + whenExtensionConfig, + dataClassConfig, + ); return writeDocument( library, diff --git a/codegen/gql_build/lib/src/utils/config.dart b/codegen/gql_build/lib/src/utils/config.dart index 4eddc5ec..6afbfe2b 100644 --- a/codegen/gql_build/lib/src/utils/config.dart +++ b/codegen/gql_build/lib/src/utils/config.dart @@ -42,6 +42,10 @@ EnumFallbackConfig enumFallbackConfig(Map config) => fallbackValueMap: enumFallbackMap(config["enum_fallbacks"]), ); +DataClassConfig dataClassConfig(Map config) => DataClassConfig( + reuseFragments: config["reuse_fragments"] == true, + ); + InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig( Map config) { final whenYamlConfig = config["when_extensions"] as YamlMap?; diff --git a/codegen/gql_code_builder/lib/data.dart b/codegen/gql_code_builder/lib/data.dart index 44193975..e14186a7 100644 --- a/codegen/gql_code_builder/lib/data.dart +++ b/codegen/gql_code_builder/lib/data.dart @@ -2,11 +2,13 @@ import "package:built_collection/built_collection.dart"; import "package:code_builder/code_builder.dart"; import "package:gql/ast.dart"; import "package:gql_code_builder/src/common.dart"; +import "package:gql_code_builder/src/config/data_class_config.dart"; import "package:gql_code_builder/src/config/when_extension_config.dart"; import "./source.dart"; import "./src/operation/data.dart"; +export "package:gql_code_builder/src/config/data_class_config.dart"; export "package:gql_code_builder/src/config/when_extension_config.dart"; Library buildDataLibrary( @@ -19,9 +21,14 @@ Library buildDataLibrary( generateWhenExtensionMethod: false, generateMaybeWhenExtensionMethod: false, ), + DataClassConfig dataClassConfig = const DataClassConfig( + reuseFragments: false, + ), ]) { final fragmentMap = _fragmentMap(docSource); - final dataClassAliasMap = _dataClassAliasMap(docSource, fragmentMap); + final dataClassAliasMap = dataClassConfig.reuseFragments + ? _dataClassAliasMap(docSource, fragmentMap) + : {}; final operationDataClasses = docSource.document.definitions .whereType() diff --git a/codegen/gql_code_builder/lib/src/config/data_class_config.dart b/codegen/gql_code_builder/lib/src/config/data_class_config.dart new file mode 100644 index 00000000..65a399e5 --- /dev/null +++ b/codegen/gql_code_builder/lib/src/config/data_class_config.dart @@ -0,0 +1,8 @@ +/// config for the optimization of data class generation. +class DataClassConfig { + final bool reuseFragments; + + const DataClassConfig({ + required this.reuseFragments, + }); +}