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
80 changes: 66 additions & 14 deletions codegen/gql_code_builder/lib/data.dart
Original file line number Diff line number Diff line change
@@ -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";
@@ -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
@@ -40,6 +44,7 @@ Library buildDataLibrary(
typeOverrides,
whenExtensionConfig,
fragmentMap,
possibleTypesMap,
dataClassAliasMap,
),
)
@@ -55,6 +60,7 @@ Library buildDataLibrary(
typeOverrides,
whenExtensionConfig,
fragmentMap,
possibleTypesMap,
dataClassAliasMap,
),
)
@@ -80,16 +86,40 @@ Map<String, SourceSelections> _fragmentMap(SourceNode source) => {
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);
});

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);
}
});

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

@@ -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,
);
}

@@ -131,25 +164,27 @@ 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];
final fragmentTypeName = getAliasTypeName(node.name.value);
aliasMap[typeRefPrefix] =
refer(fragmentTypeName, "${fragment!.url ?? ""}#data");
// print("alias $typeRefPrefix => $fragmentTypeName");
return;
}

for (final node in selectionsWithoutTypename) {
@@ -179,20 +214,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) {
@@ -202,6 +253,7 @@ void _dataClassAliasMapDFS({
selections: node.selectionSet!.selections,
fragmentMap: fragmentMap,
aliasMap: aliasMap,
possibleTypesMap: possibleTypesMap,
);
}
}
48 changes: 29 additions & 19 deletions codegen/gql_code_builder/lib/src/inline_fragment_classes.dart
Original file line number Diff line number Diff line change
@@ -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,
@@ -32,6 +33,7 @@ List<Spec> buildInlineFragmentClasses({
baseTypeName: name,
inlineFragments: inlineFragments,
config: whenExtensionConfig,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
);
return [
@@ -71,6 +73,7 @@ List<Spec> buildInlineFragmentClasses({
fragmentMap,
),
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
schemaSource: schemaSource,
type: type,
@@ -94,28 +97,35 @@ 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),
if (dataClassAliasMap.isNotEmpty)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think this condition is not correct.
Inline Fragments can add new fields in selections which can cause conflicts in extending the fragments.

It also causes the build error in the end to end test package.

When Removing this, it works again

...Map.fromEntries(superclassSelections.entries.map((e) => MapEntry(
"${e.key}__as${inlineFragment.typeCondition!.on.name.value}",
e.value))),
},
built: built,
whenExtensionConfig: whenExtensionConfig,
),
),
];
}
34 changes: 27 additions & 7 deletions codegen/gql_code_builder/lib/src/operation/data.dart
Original file line number Diff line number Diff line change
@@ -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) {
@@ -34,6 +35,7 @@ List<Spec> buildOperationDataClasses(
),
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: {},
whenExtensionConfig: whenExtensionConfig,
@@ -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(
@@ -62,6 +65,7 @@ List<Spec> buildFragmentDataClasses(
type: frag.typeCondition.on.name.value,
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: {},
built: false,
@@ -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(
@@ -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,
@@ -180,6 +186,7 @@ List<Spec> buildSelectionSetDataClasses({
type: type,
typeOverrides: typeOverrides,
fragmentMap: fragmentMap,
possibleTypesMap: possibleTypesMap,
dataClassAliasMap: dataClassAliasMap,
superclassSelections: superclassSelections,
inlineFragments: inlineFragments,
@@ -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(
@@ -292,20 +300,32 @@ 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 fragmentExpanded = {
...fragment.selections,
..._expandFragmentSpreads(fragment.selections, fragmentMap)
};
final fragmentSpreadIndex = unmerged.indexOf(node);
unmerged.forEachIndexed((selectionIndex, selection) {
if (selectionIndex > spreadIndex &&
fragment.selections.any((s) => s.hashCode == selection.hashCode)) {
duplicateIndexList.add(selectionIndex);
if (selectionIndex != fragmentSpreadIndex &&
!(selection is FieldNode && selection.name.value == "__typename") &&
fragmentExpanded.any((s) => s.hashCode == selection.hashCode)) {
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
11 changes: 8 additions & 3 deletions codegen/gql_code_builder/lib/src/when_extension.dart
Original file line number Diff line number Diff line change
@@ -40,9 +40,15 @@ Extension? inlineFragmentWhenExtension(
{required String baseTypeName,
required List<InlineFragmentNode> inlineFragments,
required InlineFragmentSpreadWhenExtensionConfig config,
required Map<String, Set<String>> possibleTypesMap,
required Map<String, Reference> dataClassAliasMap}) {
final inlineFragmentsWithTypConditions = inlineFragments
.where((inlineFragment) => inlineFragment.typeCondition != null)
.where((inlineFragment) =>
inlineFragment.typeCondition != null
// except interfaces on when extension
&&
!possibleTypesMap
.containsKey(inlineFragment.typeCondition!.on.name.value))
.toList();

final nothingToDo = inlineFragmentsWithTypConditions.isEmpty ||
@@ -187,8 +193,7 @@ Extension? inlineFragmentWhenExtension(
[
_switchTypeName,
_curlyBracketOpen,
...inlineFragments
.where((element) => element.typeCondition != null)
...inlineFragmentsWithTypConditions
.map((inlineFragment) => Block.of([
_caseStatement(inlineFragment),
maybeWhenCaseBody(inlineFragment),