Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[gql_code_builder ] bugfix for reuse-fragments feature #417

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
83 changes: 68 additions & 15 deletions codegen/gql_code_builder/lib/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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 "package:gql_code_builder/src/utils/possible_types.dart";

import "./source.dart";
import "./src/operation/data.dart";
Expand All @@ -26,8 +27,11 @@ Library buildDataLibrary(
),
]) {
final fragmentMap = _fragmentMap(docSource);
final possibleTypesMap = dataClassConfig.reuseFragments
? _possibleTypesMap(schemaSource)
: <String, Set<String>>{};
final dataClassAliasMap = dataClassConfig.reuseFragments
? _dataClassAliasMap(docSource, fragmentMap)
? _dataClassAliasMap(docSource, fragmentMap, possibleTypesMap)
: <String, Reference>{};

final operationDataClasses = docSource.document.definitions
Expand All @@ -40,6 +44,7 @@ Library buildDataLibrary(
typeOverrides,
whenExtensionConfig,
fragmentMap,
possibleTypesMap,
dataClassAliasMap,
),
)
Expand All @@ -55,6 +60,7 @@ Library buildDataLibrary(
typeOverrides,
whenExtensionConfig,
fragmentMap,
possibleTypesMap,
dataClassAliasMap,
),
)
Expand All @@ -71,25 +77,49 @@ Library buildDataLibrary(
}

Map<String, SourceSelections> _fragmentMap(SourceNode source) => {
for (var def
for (final def
in source.document.definitions.whereType<FragmentDefinitionNode>())
def.name.value: SourceSelections(
url: source.url,
selections: def.selectionSet.selections,
),
for (var import in source.imports) ..._fragmentMap(import)
for (final import in source.imports) ..._fragmentMap(import)
};

Map<String, Set<String>> _possibleTypesMap(SourceNode source,
[Set<String>? visitedSource, Map<String, Set<String>>? possibleTypesMap]) {
visitedSource ??= {};
possibleTypesMap ??= {};

source.imports.forEach((s) {
if (!visitedSource!.contains(source.url)) {
visitedSource.add(source.url);
_possibleTypesMap(s, visitedSource, possibleTypesMap);
}
});

source.document.possibleTypesMap().forEach((key, value) {
possibleTypesMap![key] ??= {};
possibleTypesMap[key]!.addAll(value);
});
dehypnosis marked this conversation as resolved.
Show resolved Hide resolved

return possibleTypesMap;
}

Map<String, Reference> _dataClassAliasMap(
SourceNode source, Map<String, SourceSelections> fragmentMap,
[Map<String, Reference>? aliasMap, Set<String>? visitedSource]) {
SourceNode source,
Map<String, SourceSelections> fragmentMap,
Map<String, Set<String>> possibleTypesMap,
[Map<String, Reference>? aliasMap,
Set<String>? visitedSource]) {
aliasMap ??= {};
visitedSource ??= {};

source.imports.forEach((s) {
if (!visitedSource!.contains(source.url)) {
visitedSource.add(source.url);
_dataClassAliasMap(s, fragmentMap, aliasMap);
_dataClassAliasMap(
s, fragmentMap, possibleTypesMap, aliasMap, visitedSource);
}
});

Expand All @@ -101,6 +131,7 @@ Map<String, Reference> _dataClassAliasMap(
selections: def.selectionSet.selections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
}

Expand All @@ -112,13 +143,15 @@ Map<String, Reference> _dataClassAliasMap(
selections: def.selectionSet.selections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
_dataClassAliasMapDFS(
typeRefPrefix: builtClassName("${def.name.value}Data"),
getAliasTypeName: (fragmentName) => "${builtClassName(fragmentName)}Data",
selections: def.selectionSet.selections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
}

Expand All @@ -131,17 +164,20 @@ void _dataClassAliasMapDFS({
required List<SelectionNode> selections,
required Map<String, SourceSelections> fragmentMap,
required Map<String, Reference> aliasMap,
required Map<String, Set<String>> possibleTypesMap,
bool shouldAlwaysGenerate = false,
}) {
if (selections.isEmpty) return;

// flatten selections to extract untouched fragments while visiting children.
// shrink 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 &&
if (!shouldAlwaysGenerate &&
selectionsWithoutTypename.length == 1 &&
selectionsWithoutTypename.first is FragmentSpreadNode) {
final node = selectionsWithoutTypename.first as FragmentSpreadNode;
final fragment = fragmentMap[node.name.value];
Expand Down Expand Up @@ -179,20 +215,36 @@ void _dataClassAliasMapDFS({
selections: exclusiveFragmentSelections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
} else if (node is InlineFragmentNode) {
/// TODO: Handle inline fragments without a type condition
if (node.typeCondition != null) {
/// TODO: Handle inline fragments without a type condition
final currentType = node.typeCondition!.on.name.value;
final interfaces = possibleTypesMap.entries
.where((e) => e.value.contains(currentType))
.map((e) => e.key)
.toSet();

final selectionsIncludingInterfaces = [
...selections.where((s) => !(s is InlineFragmentNode)),
// spread all interfaces which current type is belongs to
...selections
.whereType<InlineFragmentNode>()
.where((s) =>
s == node ||
interfaces.contains(s.typeCondition?.on.name.value))
.expand((s) => s.selectionSet.selections),
];

_dataClassAliasMapDFS(
typeRefPrefix:
"${typeRefPrefix}__as${node.typeCondition!.on.name.value}",
typeRefPrefix: "${typeRefPrefix}__as${currentType}",
getAliasTypeName: getAliasTypeName,
selections: [
...selections.where((s) => s != node),
...node.selectionSet.selections,
],
selections: selectionsIncludingInterfaces,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
shouldAlwaysGenerate: true,
);
}
} else if (node is FieldNode && node.selectionSet != null) {
Expand All @@ -202,6 +254,7 @@ void _dataClassAliasMapDFS({
selections: node.selectionSet!.selections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
}
}
Expand Down
49 changes: 29 additions & 20 deletions codegen/gql_code_builder/lib/src/inline_fragment_classes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ List<Spec> buildInlineFragmentClasses({
required String type,
required Map<String, Reference> typeOverrides,
required Map<String, SourceSelections> fragmentMap,
required Map<String, Set<String>> possibleTypesMap,
required Map<String, Reference> dataClassAliasMap,
required Map<String, SourceSelections> superclassSelections,
required List<InlineFragmentNode> inlineFragments,
Expand All @@ -32,6 +33,7 @@ List<Spec> buildInlineFragmentClasses({
baseTypeName: name,
inlineFragments: inlineFragments,
config: whenExtensionConfig,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
);
return [
Expand Down Expand Up @@ -71,6 +73,7 @@ List<Spec> buildInlineFragmentClasses({
fragmentMap,
),
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
schemaSource: schemaSource,
type: type,
Expand All @@ -94,28 +97,34 @@ List<Spec> buildInlineFragmentClasses({
// print("alias $typeName => ${dataClassAliasMap[typeName]!.symbol}");
return false;
}
// is it okay to inlcude interfaces?
return true;
}).expand(
(inlineFragment) => buildSelectionSetDataClasses(
name: "${name}__as${inlineFragment.typeCondition!.on.name.value}",
selections: mergeSelections(
[
...selections.whereType<FieldNode>(),
...selections.whereType<FragmentSpreadNode>(),
...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),
name: "${name}__as${inlineFragment.typeCondition!.on.name.value}",
selections: mergeSelections(
[
...selections.whereType<FieldNode>(),
...selections.whereType<FragmentSpreadNode>(),
...inlineFragment.selectionSet.selections,
],
fragmentMap,
),
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
schemaSource: schemaSource,
type: inlineFragment.typeCondition!.on.name.value,
typeOverrides: typeOverrides,
superclassSelections: {
name: SourceSelections(url: null, selections: selections),
...Map.fromEntries(superclassSelections.entries.map((e) => MapEntry(
"${e.key}__as${inlineFragment.typeCondition!.on.name.value}",
e.value))),
},
Copy link
Contributor Author

@dehypnosis dehypnosis Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knaeckeKami I think the cause is that this fix about buildInlineFragmentClasses is not checking the feature flag.
This fix is made to let ..FragmentData classes cover more interfaces like by adding GheroFieldsFragment__asHuman interface to GheroFieldsFragmentData__asHuman. And I think changes made by this are all additions for missing interfaces but no regressions of existing interfaces. So there would be no regression issue even without the feature flag.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just run the code generation without the flag and unfortunately, it produces code that does not compile.

the following graphql document caues problems:

query HeroWithInterfaceSubTypedFragments($episode: Episode!) {
  hero(episode: $episode) {
    ...heroFieldsFragment
  }
}

fragment heroFieldsFragment on Character {
  id
  name

  ...on Human {
    ...humanFieldsFragment
  }

  ...on Droid {
    ...droidFieldsFragment
  }
}

fragment humanFieldsFragment on Human {
  homePlanet
  friends {
    ...on Droid {
      id
      name
      ...droidFieldsFragment
    }
    ...on Human {
      id
      name
      homePlanet
    }
  }
}

fragment droidFieldsFragment on Droid {
  primaryFunction
}

this adds another superclass to GheroFieldsFragmentData__asHuman which conflicts with the implementation:

'GheroFieldsFragmentData__asHuman.friends' ('BuiltList<GheroFieldsFragmentData__asHuman_friends?>? Function()') isn't a valid override of 'GheroFieldsFragment__asHuman.friends' ('BuiltList<GheroFieldsFragment__asHuman_friends?>? Function()'). (Documentation) 
The member being overridden (hero_with_interface_subtyped_fragments.data.gql.dart:151).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knaeckeKami sorry to bother you, I will turn off new logics when there is no feature flag. Will share again soon.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no worries! i really appreciate you working on this! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knaeckeKami
How are you doing well? Finally came back here.
Sorry for late response. Just made the update. 😀

built: built,
whenExtensionConfig: whenExtensionConfig,
),
),
];
}
Expand All @@ -139,7 +148,7 @@ List<Method> _inlineFragmentRootSerializationMethods({
/// TODO: Handle inline fragments without a type condition
/// https://spec.graphql.org/June2018/#sec-Inline-Fragments
{
for (var v in inlineFragments
for (final v in inlineFragments
.where((frag) => frag.typeCondition != null))
"${v.typeCondition!.on.name.value}": dataClassAliasMap[
"${name}__as${v.typeCondition!.on.name.value}"] ??
Expand Down
28 changes: 22 additions & 6 deletions codegen/gql_code_builder/lib/src/operation/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ List<Spec> buildOperationDataClasses(
Map<String, Reference> typeOverrides,
InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig,
Map<String, SourceSelections> fragmentMap,
Map<String, Set<String>> possibleTypesMap,
Map<String, Reference> dataClassAliasMap,
) {
if (op.name == null) {
Expand All @@ -34,6 +35,7 @@ List<Spec> buildOperationDataClasses(
),
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: {},
whenExtensionConfig: whenExtensionConfig,
Expand All @@ -47,6 +49,7 @@ List<Spec> buildFragmentDataClasses(
Map<String, Reference> typeOverrides,
InlineFragmentSpreadWhenExtensionConfig whenExtensionConfig,
Map<String, SourceSelections> fragmentMap,
Map<String, Set<String>> possibleTypesMap,
Map<String, Reference> dataClassAliasMap,
) {
final selections = mergeSelections(
Expand All @@ -62,6 +65,7 @@ List<Spec> buildFragmentDataClasses(
type: frag.typeCondition.on.name.value,
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: {},
built: false,
Expand All @@ -75,6 +79,7 @@ List<Spec> buildFragmentDataClasses(
type: frag.typeCondition.on.name.value,
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: {
frag.name.value: SourceSelections(
Expand Down Expand Up @@ -120,6 +125,7 @@ List<Spec> buildSelectionSetDataClasses({
required String type,
required Map<String, Reference> typeOverrides,
required Map<String, SourceSelections> fragmentMap,
required Map<String, Set<String>> possibleTypesMap,
required Map<String, Reference> dataClassAliasMap,
required Map<String, SourceSelections> superclassSelections,
bool built = true,
Expand Down Expand Up @@ -180,6 +186,7 @@ List<Spec> buildSelectionSetDataClasses({
type: type,
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: superclassSelections,
inlineFragments: inlineFragments,
Expand Down Expand Up @@ -236,6 +243,7 @@ List<Spec> buildSelectionSetDataClasses({
name: "${name}_${field.alias?.value ?? field.name.value}",
selections: field.selectionSet!.selections,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
schemaSource: schemaSource,
type: unwrapTypeNode(
Expand Down Expand Up @@ -292,20 +300,28 @@ List<SelectionNode> shrinkSelections(
}
}

final duplicateIndices = <int>{};
for (final node in unmerged.whereType<FragmentSpreadNode>().toList()) {
if (!fragmentMap.containsKey(node.name.value)) {
throw "Cannot find a fragment ${node.name.value} from current file.";
}
final fragment = fragmentMap[node.name.value]!;
final spreadIndex = unmerged.indexOf(node);
final duplicateIndexList = <int>[];
final fragmentSpreadIndex = unmerged.indexOf(node);
unmerged.forEachIndexed((selectionIndex, selection) {
if (selectionIndex > spreadIndex &&
if (selectionIndex != fragmentSpreadIndex &&
!(selection is FieldNode && selection.name.value == "__typename") &&
fragment.selections.any((s) => s.hashCode == selection.hashCode)) {
duplicateIndexList.add(selectionIndex);
duplicateIndices.add(selectionIndex);
}
});
duplicateIndexList.reversed.forEach(unmerged.removeAt);
}

return unmerged;
return unmerged
.asMap()
.entries
.where((e) => !duplicateIndices.contains(e.key))
.map((e) => e.value)
.toList();
}

/// Deeply merges field nodes
Expand Down
Loading