Skip to content

Commit f4dbea0

Browse files
authored
refactor(interfaces): Make type predicates more robust (#9150)
* refactor(interfaces): Use typeof ... === 'function' to test for methods Testing for 'name' in object or obj.name !== undefined only checks for the existence of the property (and in the latter case that the property is not set to undefined). That's fine if the interface specifies a property of indeterminate type, but in the usual case that the interface member is a method we can do one better and check to make sure the property's value is callable. * refactor(interfaces): Always check obj is not null/undefined Since most type predicates take an argument of type any but then check for the existence of certain properties, explicitly check that the argument is not null or undefined (or check implicitly by calling another type predicate that does so first, which necessitates adding a few casts because tsc infers the type of the argument too narrowly). * fix(interfaces): Add missing check to hasBubble type predicate This appears to have inadvertently been omitted in PR #9004. * fix(interfaces): Fix misplaced typeof * fix: Fix typos in JSDocs * fix(tests): Make Mocks conform to corresponding interfaces Introduce a new MockFocusable, and add methods to MockIcon, MockBubbleIcon and MockComment, so that they fulfil the IFocusableNode, IIcon, IHasBubble and ICommentIcon interfaces respectively. * chore(tests): Add assertions verifying mocks conform to predicates Add (test) runtime assertions that: - isFocusableNode(MockFocusable) returns true - isIcon(MockIcon) returns true - hasBubble(MockBubbleIcon) returns true - isCommentIcon(MockCommentIcon) returns true (The latter is currently failing because Blockly is undefined when isCommentIcon calls the MockCommentIcon's getType method.) * fix(tests): Don't rely on Blockly being set in Mock methods For some reason the global Blockly binding is not visible at the time when isCommentIcon calls MockCommentIcon's getType method, and presumably this problem would apply to getBubbleSize too, so directly import the required items. * refactor(tests): Make MockCommentIcon a MockBubbleIcon This slightly simplifies it and makes it less likely to accidentally stop conforming to IHasBubble. * fix(interfaces): Fix incorrect check in isSelectable Fix an error which caused ISelectable instances to fail isSelectable() checks, one of the results of which is that Blockly.common.getSelected() would generally return null. Whoops!
1 parent eaf5eea commit f4dbea0

18 files changed

Lines changed: 128 additions & 80 deletions

core/interfaces/i_autohideable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,5 @@ export interface IAutoHideable extends IComponent {
2323

2424
/** Returns true if the given object is autohideable. */
2525
export function isAutoHideable(obj: any): obj is IAutoHideable {
26-
return obj.autoHide !== undefined;
26+
return obj && typeof obj.autoHide === 'function';
2727
}

core/interfaces/i_comment_icon.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,17 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable {
3131
}
3232

3333
/** Checks whether the given object is an ICommentIcon. */
34-
export function isCommentIcon(obj: object): obj is ICommentIcon {
34+
export function isCommentIcon(obj: any): obj is ICommentIcon {
3535
return (
3636
isIcon(obj) &&
3737
hasBubble(obj) &&
3838
isSerializable(obj) &&
39-
(obj as any)['setText'] !== undefined &&
40-
(obj as any)['getText'] !== undefined &&
41-
(obj as any)['setBubbleSize'] !== undefined &&
42-
(obj as any)['getBubbleSize'] !== undefined &&
43-
(obj as any)['setBubbleLocation'] !== undefined &&
44-
(obj as any)['getBubbleLocation'] !== undefined &&
39+
typeof (obj as any).setText === 'function' &&
40+
typeof (obj as any).getText === 'function' &&
41+
typeof (obj as any).setBubbleSize === 'function' &&
42+
typeof (obj as any).getBubbleSize === 'function' &&
43+
typeof (obj as any).setBubbleLocation === 'function' &&
44+
typeof (obj as any).getBubbleLocation === 'function' &&
4545
obj.getType() === IconType.COMMENT
4646
);
4747
}

core/interfaces/i_copyable.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ export type ICopyData = ICopyable.ICopyData;
3535

3636
/** @returns true if the given object is an ICopyable. */
3737
export function isCopyable(obj: any): obj is ICopyable<ICopyData> {
38-
return obj.toCopyData !== undefined;
38+
return obj && typeof obj.toCopyData === 'function';
3939
}

core/interfaces/i_deletable.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ export interface IDeletable {
2727
/** Returns whether the given object is an IDeletable. */
2828
export function isDeletable(obj: any): obj is IDeletable {
2929
return (
30-
obj['isDeletable'] !== undefined &&
31-
obj['dispose'] !== undefined &&
32-
obj['setDeleteStyle'] !== undefined
30+
obj &&
31+
typeof obj.isDeletable === 'function' &&
32+
typeof obj.dispose === 'function' &&
33+
typeof obj.setDeleteStyle === 'function'
3334
);
3435
}

core/interfaces/i_draggable.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,12 @@ export interface IDragStrategy {
6262
/** Returns whether the given object is an IDraggable or not. */
6363
export function isDraggable(obj: any): obj is IDraggable {
6464
return (
65-
obj.getRelativeToSurfaceXY !== undefined &&
66-
obj.isMovable !== undefined &&
67-
obj.startDrag !== undefined &&
68-
obj.drag !== undefined &&
69-
obj.endDrag !== undefined &&
70-
obj.revertDrag !== undefined
65+
obj &&
66+
typeof obj.getRelativeToSurfaceXY === 'function' &&
67+
typeof obj.isMovable === 'function' &&
68+
typeof obj.startDrag === 'function' &&
69+
typeof obj.drag === 'function' &&
70+
typeof obj.endDrag === 'function' &&
71+
typeof obj.revertDrag === 'function'
7172
);
7273
}

core/interfaces/i_focusable_node.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,16 @@ export interface IFocusableNode {
102102
* Determines whether the provided object fulfills the contract of
103103
* IFocusableNode.
104104
*
105-
* @param object The object to test.
105+
* @param obj The object to test.
106106
* @returns Whether the provided object can be used as an IFocusableNode.
107107
*/
108-
export function isFocusableNode(object: any | null): object is IFocusableNode {
108+
export function isFocusableNode(obj: any): obj is IFocusableNode {
109109
return (
110-
object &&
111-
'getFocusableElement' in object &&
112-
'getFocusableTree' in object &&
113-
'onNodeFocus' in object &&
114-
'onNodeBlur' in object &&
115-
'canBeFocused' in object
110+
obj &&
111+
typeof obj.getFocusableElement === 'function' &&
112+
typeof obj.getFocusableTree === 'function' &&
113+
typeof obj.onNodeFocus === 'function' &&
114+
typeof obj.onNodeBlur === 'function' &&
115+
typeof obj.canBeFocused === 'function'
116116
);
117117
}

core/interfaces/i_focusable_tree.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,17 @@ export interface IFocusableTree {
128128
* Determines whether the provided object fulfills the contract of
129129
* IFocusableTree.
130130
*
131-
* @param object The object to test.
131+
* @param obj The object to test.
132132
* @returns Whether the provided object can be used as an IFocusableTree.
133133
*/
134-
export function isFocusableTree(object: any | null): object is IFocusableTree {
134+
export function isFocusableTree(obj: any): obj is IFocusableTree {
135135
return (
136-
object &&
137-
'getRootFocusableNode' in object &&
138-
'getRestoredFocusableNode' in object &&
139-
'getNestedTrees' in object &&
140-
'lookUpFocusableNode' in object &&
141-
'onTreeFocus' in object &&
142-
'onTreeBlur' in object
136+
obj &&
137+
typeof obj.getRootFocusableNode === 'function' &&
138+
typeof obj.getRestoredFocusableNode === 'function' &&
139+
typeof obj.getNestedTrees === 'function' &&
140+
typeof obj.lookUpFocusableNode === 'function' &&
141+
typeof obj.onTreeFocus === 'function' &&
142+
typeof obj.onTreeBlur === 'function'
143143
);
144144
}

core/interfaces/i_has_bubble.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export interface IHasBubble {
3030
/** Type guard that checks whether the given object is a IHasBubble. */
3131
export function hasBubble(obj: any): obj is IHasBubble {
3232
return (
33-
obj.bubbleIsVisible !== undefined && obj.setBubbleVisible !== undefined
33+
typeof obj.bubbleIsVisible === 'function' &&
34+
typeof obj.setBubbleVisible === 'function' &&
35+
typeof obj.getBubble === 'function'
3436
);
3537
}

core/interfaces/i_icon.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,19 +98,19 @@ export interface IIcon extends IFocusableNode {
9898
/** Type guard that checks whether the given object is an IIcon. */
9999
export function isIcon(obj: any): obj is IIcon {
100100
return (
101-
obj.getType !== undefined &&
102-
obj.initView !== undefined &&
103-
obj.dispose !== undefined &&
104-
obj.getWeight !== undefined &&
105-
obj.getSize !== undefined &&
106-
obj.applyColour !== undefined &&
107-
obj.hideForInsertionMarker !== undefined &&
108-
obj.updateEditable !== undefined &&
109-
obj.updateCollapsed !== undefined &&
110-
obj.isShownWhenCollapsed !== undefined &&
111-
obj.setOffsetInBlock !== undefined &&
112-
obj.onLocationChange !== undefined &&
113-
obj.onClick !== undefined &&
114-
isFocusableNode(obj)
101+
isFocusableNode(obj) &&
102+
typeof (obj as IIcon).getType === 'function' &&
103+
typeof (obj as IIcon).initView === 'function' &&
104+
typeof (obj as IIcon).dispose === 'function' &&
105+
typeof (obj as IIcon).getWeight === 'function' &&
106+
typeof (obj as IIcon).getSize === 'function' &&
107+
typeof (obj as IIcon).applyColour === 'function' &&
108+
typeof (obj as IIcon).hideForInsertionMarker === 'function' &&
109+
typeof (obj as IIcon).updateEditable === 'function' &&
110+
typeof (obj as IIcon).updateCollapsed === 'function' &&
111+
typeof (obj as IIcon).isShownWhenCollapsed === 'function' &&
112+
typeof (obj as IIcon).setOffsetInBlock === 'function' &&
113+
typeof (obj as IIcon).onLocationChange === 'function' &&
114+
typeof (obj as IIcon).onClick === 'function'
115115
);
116116
}

core/interfaces/i_legacy_procedure_blocks.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export interface LegacyProcedureDefBlock {
2828

2929
/** @internal */
3030
export function isLegacyProcedureDefBlock(
31-
block: object,
32-
): block is LegacyProcedureDefBlock {
33-
return (block as any).getProcedureDef !== undefined;
31+
obj: any,
32+
): obj is LegacyProcedureDefBlock {
33+
return obj && typeof obj.getProcedureDef === 'function';
3434
}
3535

3636
/** @internal */
@@ -41,10 +41,11 @@ export interface LegacyProcedureCallBlock {
4141

4242
/** @internal */
4343
export function isLegacyProcedureCallBlock(
44-
block: object,
45-
): block is LegacyProcedureCallBlock {
44+
obj: any,
45+
): obj is LegacyProcedureCallBlock {
4646
return (
47-
(block as any).getProcedureCall !== undefined &&
48-
(block as any).renameProcedure !== undefined
47+
obj &&
48+
typeof obj.getProcedureCall === 'function' &&
49+
typeof obj.renameProcedure === 'function'
4950
);
5051
}

0 commit comments

Comments
 (0)