Skip to content

Provide static checking by refactoring how the service extensions used by layout_explorer are defined. - #1682

Merged
jacob314 merged 3 commits into
flutter:masterfrom
jacob314:polyfill
Mar 12, 2020
Merged

jacob314 merged 3 commits into
flutter:masterfrom
jacob314:polyfill

Conversation

@jacob314

Copy link
Copy Markdown
Contributor

Previously these extensions were defined as string literals which eliminated static checking.
They are now defined as a slightly magical dart file under assets/scripts
which gives you full dart code completion and analysis server error checking.
With this change, writing these polyfills is simple enough that we should
write all new widget_inspector.dart functionality here rather than in
package:flutter/src/widgets/widget_inspector.dart only moving it to
widget_inspector.dart once we are confident we will use the feature for the long term.


void addServiceExtensions() {
// INSPECTOR_POLYFILL_SCRIPT_START
T toEnumEntry<T>(List<T> enumEntries, String name) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

None of this code is new. It is just moved from the existing code snippets within source code that @adalberht already defined.

@jacob314 jacob314 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fyi @adalberht your existing extensions were easy to refactor this way so that I could get better static checking.

return Future.value(<String, Object>{'result': succeed});
}

void registerHelper(String name, ServiceExtensionCallback callback) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

except for this method.. it is new.

// features should be exposed with a wide range of Flutter versions to get
// useful user feedback.
//
// We current execute this library using eval but that should be viewed as an

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: r/current/currently

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

// It is not a fatal error if some of the extensions fail to register
// as could be the case if some are already defined directly within
// package:flutter for the version of package:flutter being used.
print('Warning: unable to register service extension \'$name\'');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you use the DevTools logger here instead of print?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

you can't use it directly as this code is executed on the users device. However I have now plumbed it through so errors are tracked and logged back in DevTools. Did a little bit of other refactoring to make it happen.

@albertusdev

Copy link
Copy Markdown
Contributor

This is so cool 🥺

@kenzieschmoll

Copy link
Copy Markdown
Member

lgtm_army

Comment on lines 77 to +90
class RegistrableServiceExtension {
RegistrableServiceExtension({
@required this.name,
@required this.statements,
this.requireEnumDeserialization = false,
});

final String name;

/// Statements inside the callback.
/// Should end with ';'
/// To deserialize the required parameters, use variable 'parameters'.
/// See [getLayoutExplorerNode] for example.
final String statements;

// Whether this callback need to deserialize enum values or not.
final bool requireEnumDeserialization;

// Generated ServiceExtensionCallback definition
String get callbackDefinition {
return '''
${requireEnumDeserialization ? toEnumEntryCodeDefinition : ''}
Future<Map<String, dynamic>> $name(Map<String, String> parameters){
$statements
}
''';
}

static final getLayoutExplorerNode = RegistrableServiceExtension(
name: 'getLayoutExplorerNode',
statements: '''
final String id = parameters['id'];
final int subtreeDepth = int.parse(parameters['subtreeDepth']);
final String groupName = parameters['groupName'];
Map<String, Object> result = {};
final instance = WidgetInspectorService.instance;
final root = instance.toObject(id);
if (root == null) {
result = null;
} else {
result = instance._nodeToJson(
root,
InspectorSerializationDelegate(
groupName: groupName,
summaryTree: true,
subtreeDepth: subtreeDepth,
includeProperties: false,
service: instance,
addAdditionalPropertiesCallback: (node, delegate) {
final Map<String, Object> additionalJson = <String, Object>{};
final Object value = node.value;
if (value is Element) {
final renderObject = value.renderObject;
additionalJson['renderObject'] =
renderObject.toDiagnosticsNode()?.toJsonMap(
delegate.copyWith(
subtreeDepth: 0,
includeProperties: true,
),
);
final Constraints constraints = renderObject.constraints;
if (constraints != null) {
final Map<String, Object> constraintsProperty = <
String,
Object>{
'type': constraints.runtimeType.toString(),
'description': constraints.toString(),
};
if (constraints is BoxConstraints) {
constraintsProperty.addAll(<String, Object>{
'minWidth': constraints.minWidth.toString(),
'minHeight': constraints.minHeight.toString(),
'maxWidth': constraints.maxWidth.toString(),
'maxHeight': constraints.maxHeight.toString(),
});
}
additionalJson['constraints'] = constraintsProperty;
}
if (renderObject is RenderBox) {
additionalJson['size'] = <String, Object>{
'width': renderObject.size.width.toString(),
'height': renderObject.size.height.toString(),
};

final ParentData parentData = renderObject.parentData;
if (parentData is FlexParentData) {
additionalJson['flexFactor'] = parentData.flex;
additionalJson['flexFit'] =
describeEnum(parentData.fit ?? FlexFit.tight);
}
}
}
return additionalJson;
}
),
);
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': result,
});
''',
);

static final setFlexFit = RegistrableServiceExtension(
name: 'setFlexFit',
statements: '''
final String id = parameters['id'];
final FlexFit flexFit =
toEnumEntry<FlexFit>(FlexFit.values, parameters['flexFit']);
dynamic object = WidgetInspectorService.instance.toObject(id);
if (object == null) return null;
final render = object.renderObject;
final parentData = render.parentData;
bool succeed = false;
if (parentData is FlexParentData) {
parentData.fit = flexFit;
render.markNeedsLayout();
succeed = true;
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed,
});
''',
requireEnumDeserialization: true,
);
static final setFlexFactor = RegistrableServiceExtension(
name: 'setFlexFactor',
statements: '''
final String id = parameters['id'];
final String flexFactor = parameters['flexFactor'];
final int factor = flexFactor == "null" ? null : int.parse(flexFactor);
final dynamic object = WidgetInspectorService.instance.toObject(id);
if (object == null) return null;
final render = object.renderObject;
final parentData = render.parentData;
bool succeed = false;
if (parentData is FlexParentData) {
parentData.flex = factor;
render.markNeedsLayout();
succeed = true;
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed,
});
''',
);
static final setFlexProperties = RegistrableServiceExtension(
name: 'setFlexProperties',
statements: '''
final String id = parameters['id'];
final MainAxisAlignment mainAxisAlignment = toEnumEntry<MainAxisAlignment>(
MainAxisAlignment.values,
parameters['mainAxisAlignment'],
);
final CrossAxisAlignment crossAxisAlignment = toEnumEntry<CrossAxisAlignment>(
CrossAxisAlignment.values,
parameters['crossAxisAlignment'],
);
final dynamic object = WidgetInspectorService.instance.toObject(id);
if (object == null) return null;
final render = object.renderObject;
bool succeed = false;
if (render is RenderFlex) {
render.mainAxisAlignment = mainAxisAlignment;
render.crossAxisAlignment = crossAxisAlignment;
render.markNeedsLayout();
succeed = true;
}
return Future<Map<String, Object>>.value(<String, Object>{
'result': succeed,
});
''',
requireEnumDeserialization: true,
);
static final getLayoutExplorerNode =
RegistrableServiceExtension(name: 'getLayoutExplorerNode');
static final setFlexFit = RegistrableServiceExtension(name: 'setFlexFit');
static final setFlexFactor =
RegistrableServiceExtension(name: 'setFlexFactor');
static final setFlexProperties =
RegistrableServiceExtension(name: 'setFlexProperties');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this class can be removed now and be replaced with pure string

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm leaving it for now because I expect we'll add some more logic back to it at some point tracking minimum versions required or similar.

jacob314 and others added 2 commits March 12, 2020 09:12
… for static checking.

Previously these extensions were defined as string literals which eliminated static checking.
They are now defined as a slightly magical dart file under assets/scripts
which gives you full dart code completion and analysis server error checking.
With this change, writing these polyfills is simple enough that we should
write all new widget_inspector.dart functionality here rather than in
package:flutter/src/widgets/widget_inspector.dart only moving it to
widget_inspector.dart once we are confident we will use the feature for the long term.
@jacob314

Copy link
Copy Markdown
Contributor Author

ptal

registerHelper('setFlexFit', setFlexFit);
registerHelper('setFlexFactor', setFlexFactor);
registerHelper('setFlexProperties', setFlexProperties);
return failures.isNotEmpty

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why is this returning at all when the method signature return is void

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it isn't void. it is String addServiceExtensions() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

oh whoops, missed a bracket.

return stringRef.valueAsString;

final dynamic result = await getObject(isolateId, stringRef.id,
offset: 0, count: stringRef.length);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

trailing comma

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

String onUnavailable(String truncatedValue),
}) async {
if (stringRef == null) return null;
if (stringRef.valueAsStringIsTruncated != true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

you can just write !stringRef.valueAsStringIsTruncated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

valueAsStringIsTruncated looks like it may be null. would be nice to have null safety so this didn't matter.
expect when we get null safety support there will be a lint for this case.
for now I'd rather just keep this consistent with what this method looked like back before I moved it to a central location.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sg

if (stringRef.valueAsStringIsTruncated != true)
return stringRef.valueAsString;

final dynamic result = await getObject(isolateId, stringRef.id,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

do you have to specify dynamic here for type checking to work below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this method is just being moved to a central location.
however I agree with the cleanup. removed the dynamic and used type inference.

final dynamic result = await getObject(isolateId, stringRef.id,
offset: 0, count: stringRef.length);
if (result is Instance) {
final Instance obj = result;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this type necessary? Does the type promotion not happen on its own?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep. this code was written a long time ago.

String onUnavailable(String truncatedValue),
}) async {
if (stringRef == null) return null;
if (stringRef.valueAsStringIsTruncated != true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sg

@jacob314
jacob314 merged commit ab727ae into flutter:master Mar 12, 2020
@jacob314
jacob314 deleted the polyfill branch March 12, 2020 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants