Skip to content

Commit

Permalink
Make graph work with tearoffs - add tooltips (#2786)
Browse files Browse the repository at this point in the history
  • Loading branch information
freemansoft authored Aug 14, 2023
1 parent 60aaaf8 commit 4b0d262
Show file tree
Hide file tree
Showing 9 changed files with 319 additions and 77 deletions.
2 changes: 1 addition & 1 deletion examples/random_number/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ class RandomNumberApp extends StatelessWidget {
}
}

//Custom consumer using the provider
/// Custom consumer using the provider
class RandomConsumer extends ConsumerWidget {
const RandomConsumer({Key? key}) : super(key: key);

Expand Down
1 change: 1 addition & 0 deletions examples/todos/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ class Title extends StatelessWidget {
/// impacted widgets rebuilds, instead of the entire list of items.
final _currentTodo = Provider<Todo>((ref) => throw UnimplementedError());

/// The widget that that displays the components of an individual Todo Item
class TodoItem extends HookConsumerWidget {
const TodoItem({Key? key}) : super(key: key);

Expand Down
5 changes: 3 additions & 2 deletions packages/riverpod_graph/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
This project is a work-in-progress command that analyzes a Riverpod project and
generates a graph of the interactions between providers/widgets

The graph generated is generated using [Mermaid](https://mermaid-js.github.io/mermaid/#/)
## Example
Graphs can be generated using [d2](https://d2lang.com/) or [Mermaid](https://mermaid-js.github.io/mermaid/#/) text-to-graph syntax.

Here is graph example, generated from the Flutter Devtool project (which uses Riverpod).

Expand Down Expand Up @@ -33,4 +34,4 @@ mermaid.js markup
```bash
cd <the lib directory of the program you wish to analyze>
$ dart pub global run riverpod_graph .
```
```
97 changes: 80 additions & 17 deletions packages/riverpod_graph/lib/src/analyze.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ enum SupportFormat {
d2,
}

/// used to wrap names in d2 diagram boxes
const rawNewline = r'\n';

String _buildD2(ProviderGraph providerGraph) {
const _watchLineStyle = '{style.stroke-width: 4}';
const _readLineStyle = '{style.stroke-dash: 4}';
Expand All @@ -119,19 +122,48 @@ String _buildD2(ProviderGraph providerGraph) {
Legend: {
Type: {
Widget.shape: circle
Provider
Provider: rectangle
}
Arrows: {
"." -> "..": read: {style.stroke-dash: 4}
"." -> "..": listen
"." -> "..": watch: {style.stroke-width: 4}
}
}
''');

// declare all the provider nodes before doing any connections
// this lets us do all node config in one place before they are used
for (final node in providerGraph.providers) {
final providerName = _displayNameForProvider(node.definition).name;
buffer.writeln('$providerName: "$providerName"');
buffer.writeln('$providerName.shape: rectangle');

// d2 supports tooltips. mermaid does not
// add the first line of any documentation comment as a tooltip
final docComment = _displayDocCommentForProvider(node.definition);
if (docComment != null) {
buffer.writeln('$providerName.tooltip: "$docComment"');
}
}

// declare all the widget nodes before doing any connections
// this lets us do all node config in one place before they are used
for (final node in providerGraph.consumerWidgets) {
buffer.writeln('${node.definition.name}.shape: Circle');
final widgetName = node.definition.name;
buffer.writeln('$widgetName.shape: circle');

// d2 supports tooltips. mermaid does not
// add the first line of any documentation comment as a tooltip
final docComment = _displayDocCommentForWidget(node.definition);
if (docComment != null) {
buffer.writeln('$widgetName.tooltip: "$docComment"');
}
}
buffer.writeln();

for (final node in providerGraph.consumerWidgets) {
for (final watch in node.watch) {
buffer.writeln(
'${_displayNameForProvider(watch.definition).name} -> ${node.definition.name}: $_watchLineStyle',
Expand Down Expand Up @@ -187,17 +219,41 @@ flowchart TB
style start3 height:0px;
style stop3 height:0px;
end
subgraph Type
direction TB
ConsumerWidget((widget));
Provider[[provider]];
end
''');

// declare all the provider nodes before doing any connections
// this lets us do all node config in one place before they are used
for (final node in providerGraph.providers) {
final nodeGlobalName = _displayNameForProvider(node.definition);
final isContainedInClass = nodeGlobalName.enclosingElementName.isNotEmpty;

if (isContainedInClass) {
buffer.writeln(' subgraph ${nodeGlobalName.enclosingElementName}');
buffer.writeln(
' ${nodeGlobalName.name}[["${nodeGlobalName.providerName}"]];',
);
buffer.writeln(' end');
} else {
buffer.writeln(
' ${nodeGlobalName.name}[["${nodeGlobalName.providerName}"]];',
);
}
}

// declare all the widget nodes before doing any connections
// this lets us do all node config in one place before they are used
for (final node in providerGraph.consumerWidgets) {
buffer.writeln(' ${node.definition.name}((${node.definition.name}));');
}
buffer.writeln();

for (final node in providerGraph.consumerWidgets) {
for (final watch in node.watch) {
buffer.writeln(
' ${_displayNameForProvider(watch.definition).name} ==> ${node.definition.name};',
Expand All @@ -217,18 +273,6 @@ flowchart TB

for (final node in providerGraph.providers) {
final nodeGlobalName = _displayNameForProvider(node.definition);
final isContainedInClass = nodeGlobalName.enclosingElementName.isNotEmpty;
if (isContainedInClass) {
buffer.writeln(' subgraph ${nodeGlobalName.enclosingElementName}');
buffer.writeln(
' ${nodeGlobalName.name}[[${nodeGlobalName.providerName}]];',
);
buffer.writeln(' end');
} else {
buffer.writeln(
' ${nodeGlobalName.name}[[${nodeGlobalName.providerName}]];',
);
}

final providerName = nodeGlobalName.providerName;
for (final watch in node.watch) {
Expand Down Expand Up @@ -431,14 +475,17 @@ class ProviderDependencyVisitor extends RecursiveAstVisitor<void> {
)
?.node;
if (classDeclaration is ClassDeclaration) {
// firstWhereOrNull required if a class was created with .new
final buildMethod = classDeclaration.members
.whereType<MethodDeclaration>()
.firstWhere(
.firstWhereOrNull(
(method) => method.name.lexeme == 'build',
);
// Instead of continuing with the current node, we visit the one of
// the referenced constructor.
return buildMethod.visitChildren(this);
if (buildMethod != null) {
return buildMethod.visitChildren(this);
}
}
}
}
Expand Down Expand Up @@ -643,6 +690,22 @@ _ProviderName _displayNameForProvider(VariableElement provider) {
);
}

String? _displayDocCommentForProvider(VariableElement definition) {
return definition.documentationComment
// this will show no text if the first doc comment line is blank :-(
// tooltips should be short
?.split('\n')[0]
.replaceAll('/// ', '');
}

String? _displayDocCommentForWidget(ClassElement definition) {
return definition.documentationComment
// this will show no text if the first doc comment line is blank :-(
// tooltips should be short
?.split('\n')[0]
.replaceAll('/// ', '');
}

/// Returns the variable element of the watched/listened/read `provider` in an expression. For example:
/// - `watch(family(0).modifier)`
/// - `watch(provider.modifier)`
Expand Down
113 changes: 86 additions & 27 deletions packages/riverpod_graph/test/integration/addition/addition_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ void main() {
}

expect(
stdoutList.first,
// replace windows file separator with linux - make test pass on windows
stdoutList.first.replaceAll(r'\', '/'),
allOf(
[
startsWith('Analyzing'),
Expand All @@ -38,18 +39,41 @@ flowchart TB
style stop1 height:0px;
start2[ ] --->|listen| stop2[ ]
style start2 height:0px;
style stop2 height:0px;
style stop2 height:0px;
start3[ ] ===>|watch| stop3[ ]
style start3 height:0px;
style stop3 height:0px;
style stop3 height:0px;
end
subgraph Type
direction TB
ConsumerWidget((widget));
Provider[[provider]];
end
additionProvider[[additionProvider]];
additionProvider[["additionProvider"]];
normalProvider[["normalProvider"]];
futureProvider[["futureProvider"]];
familyProviders[["familyProviders"]];
functionProvider[["functionProvider"]];
selectedProvider[["selectedProvider"]];
subgraph SampleClass
SampleClass.normalProvider[["normalProvider"]];
end
subgraph SampleClass
SampleClass.futureProvider[["futureProvider"]];
end
subgraph SampleClass
SampleClass.familyProviders[["familyProviders"]];
end
subgraph SampleClass
SampleClass.functionProvider[["functionProvider"]];
end
subgraph SampleClass
SampleClass.selectedProvider[["selectedProvider"]];
end
marvelTearOffConsumer[["marvelTearOffConsumer"]];
marvelRefdProvider[["marvelRefdProvider"]];
normalProvider ==> additionProvider;
futureProvider ==> additionProvider;
familyProviders ==> additionProvider;
Expand All @@ -60,26 +84,7 @@ flowchart TB
SampleClass.familyProviders ==> additionProvider;
SampleClass.functionProvider ==> additionProvider;
SampleClass.selectedProvider ==> additionProvider;
normalProvider[[normalProvider]];
futureProvider[[futureProvider]];
familyProviders[[familyProviders]];
functionProvider[[functionProvider]];
selectedProvider[[selectedProvider]];
subgraph SampleClass
SampleClass.normalProvider[[normalProvider]];
end
subgraph SampleClass
SampleClass.futureProvider[[futureProvider]];
end
subgraph SampleClass
SampleClass.familyProviders[[familyProviders]];
end
subgraph SampleClass
SampleClass.functionProvider[[functionProvider]];
end
subgraph SampleClass
SampleClass.selectedProvider[[selectedProvider]];
end''',
marvelRefdProvider -.-> marvelTearOffConsumer;''',
reason: 'It should log the riverpod graph',
);
await process.shouldExit(0);
Expand All @@ -104,7 +109,8 @@ flowchart TB
}

expect(
stdoutList.first,
// replace windows file separator with linux - make test pass on windows
stdoutList.first.replaceAll(r'\', '/'),
allOf(
[
startsWith('Analyzing'),
Expand All @@ -119,6 +125,58 @@ flowchart TB
expect(
stdoutList.sublist(1).join('\n'),
'''
Legend: {
Type: {
Widget.shape: circle
Provider: rectangle
}
Arrows: {
"." -> "..": read: {style.stroke-dash: 4}
"." -> "..": listen
"." -> "..": watch: {style.stroke-width: 4}
}
}
additionProvider: "additionProvider"
additionProvider.shape: rectangle
additionProvider.tooltip: "A provider returning the sum of the other providers."
normalProvider: "normalProvider"
normalProvider.shape: rectangle
normalProvider.tooltip: "A provider returning a number."
futureProvider: "futureProvider"
futureProvider.shape: rectangle
futureProvider.tooltip: "A future provider returning a number."
familyProviders: "familyProviders"
familyProviders.shape: rectangle
familyProviders.tooltip: "A family provider returning a number."
functionProvider: "functionProvider"
functionProvider.shape: rectangle
functionProvider.tooltip: "A provider returning a function that returns a number."
selectedProvider: "selectedProvider"
selectedProvider.shape: rectangle
selectedProvider.tooltip: "A provider returning a number that will be selected."
SampleClass.normalProvider: "SampleClass.normalProvider"
SampleClass.normalProvider.shape: rectangle
SampleClass.normalProvider.tooltip: "A provider returning a number."
SampleClass.futureProvider: "SampleClass.futureProvider"
SampleClass.futureProvider.shape: rectangle
SampleClass.futureProvider.tooltip: "A future provider returning a number."
SampleClass.familyProviders: "SampleClass.familyProviders"
SampleClass.familyProviders.shape: rectangle
SampleClass.familyProviders.tooltip: "A family provider returning a number."
SampleClass.functionProvider: "SampleClass.functionProvider"
SampleClass.functionProvider.shape: rectangle
SampleClass.functionProvider.tooltip: "A provider returning a function that returns a number."
SampleClass.selectedProvider: "SampleClass.selectedProvider"
SampleClass.selectedProvider.shape: rectangle
SampleClass.selectedProvider.tooltip: "A provider returning a number that will be selected."
marvelTearOffConsumer: "marvelTearOffConsumer"
marvelTearOffConsumer.shape: rectangle
marvelTearOffConsumer.tooltip: "read/watch/listen seem to be required to bring this in scope for analysis"
marvelRefdProvider: "marvelRefdProvider"
marvelRefdProvider.shape: rectangle
marvelRefdProvider.tooltip: "taken from the marvel example"
normalProvider -> additionProvider: {style.stroke-width: 4}
futureProvider -> additionProvider: {style.stroke-width: 4}
familyProviders -> additionProvider: {style.stroke-width: 4}
Expand All @@ -128,7 +186,8 @@ SampleClass.normalProvider -> additionProvider: {style.stroke-width: 4}
SampleClass.futureProvider -> additionProvider: {style.stroke-width: 4}
SampleClass.familyProviders -> additionProvider: {style.stroke-width: 4}
SampleClass.functionProvider -> additionProvider: {style.stroke-width: 4}
SampleClass.selectedProvider -> additionProvider: {style.stroke-width: 4}''',
SampleClass.selectedProvider -> additionProvider: {style.stroke-width: 4}
marvelRefdProvider -> marvelTearOffConsumer: {style.stroke-dash: 4}''',
reason: 'It should log the riverpod graph',
);
await process.shouldExit(0);
Expand Down
Loading

0 comments on commit 4b0d262

Please sign in to comment.