Skip to content

Commit 5404d79

Browse files
ca-dstee-re
authored andcommitted
fix: increase type guard vigilance
1 parent bcf9e3a commit 5404d79

File tree

7 files changed

+106
-84
lines changed

7 files changed

+106
-84
lines changed

convertEdit.spec.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
/* eslint-disable @typescript-eslint/no-unused-expressions */
22
import { expect } from '@open-wc/testing';
33

4-
import { Insert, Remove, Update } from './editv1.js';
4+
import { Update } from './editv1.js';
55

66
import { convertEdit } from './convertEdit.js';
7-
import { SetAttributes } from './editv2.js';
7+
import { Insert, Remove, SetAttributes } from './editv2.js';
88

99
const doc = new DOMParser().parseFromString(
1010
'<SCL><Substation name="AA1"></Substation></SCL>',
@@ -26,7 +26,6 @@ const update: Update = {
2626
attributes: {
2727
name: 'A2',
2828
desc: null,
29-
['__proto__']: 'a string',
3029
'myns:attr': {
3130
value: 'value1',
3231
namespaceURI: 'http://example.org/myns',
@@ -43,9 +42,9 @@ const update: Update = {
4342
value: 'value2',
4443
namespaceURI: 'http://example.org/myns2',
4544
},
46-
attr3: {
47-
value: 'value3',
48-
namespaceURI: null,
45+
invalid: {
46+
value: 'great, but with empty namespace URI',
47+
namespaceURI: '',
4948
},
5049
},
5150
};

convertEdit.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,16 @@
1+
import { Edit, isComplex, isNamespaced, isUpdate, Update } from './editv1.js';
2+
13
import {
2-
Edit,
3-
isComplex,
4+
Attributes,
5+
AttributesNS,
6+
EditV2,
47
isInsert,
5-
isNamespaced,
6-
isUpdate,
78
isRemove,
8-
Update,
9-
} from './editv1.js';
10-
11-
import { EditV2 } from './editv2.js';
9+
} from './editv2.js';
1210

1311
function convertUpdate(edit: Update): EditV2 {
14-
const attributes: Partial<Record<string, string | null>> = {};
15-
const attributesNS: Partial<
16-
Record<string, Partial<Record<string, string | null>>>
17-
> = {};
12+
const attributes: Attributes = {};
13+
const attributesNS: AttributesNS = {};
1814

1915
Object.entries(edit.attributes).forEach(([key, value]) => {
2016
if (isNamespaced(value!)) {

editv1.spec.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,11 @@ describe('type guard functions for editv1', () => {
2828

2929
it('returns true for Remove', () => expect(remove).to.satisfy(isEdit));
3030

31-
it('returns false for SetAttributes', () =>
32-
expect(setAttributes).to.not.satisfy(isEdit));
33-
3431
it('returns true for SetTextContent', () =>
3532
expect(setTextContent).to.not.satisfy(isEdit));
3633

3734
it('returns false on mixed edit and editV2 array', () =>
38-
expect([update, setAttributes]).to.not.satisfy(isEdit));
35+
expect([update, setTextContent]).to.not.satisfy(isEdit));
3936

4037
it('returns true on edit array', () =>
4138
expect([update, remove, insert]).to.satisfy(isEdit));

editv1.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,67 @@
1-
import { isSetAttributes } from './editv2.js';
2-
3-
/** Intent to `parent.insertBefore(node, reference)` */
4-
export type Insert = {
5-
parent: Node;
6-
node: Node;
7-
reference: Node | null;
8-
};
1+
import { isInsert, isRemove, Insert, Remove } from './editv2.js';
92

103
export type NamespacedAttributeValue = {
114
value: string | null;
125
namespaceURI: string | null;
136
};
7+
148
export type AttributeValue = string | null | NamespacedAttributeValue;
9+
10+
export type AttributesV1 = Partial<Record<string, AttributeValue>>;
11+
1512
/** Intent to set or remove (if null) attributes on element */
1613
export type Update = {
1714
element: Element;
1815
attributes: Partial<Record<string, AttributeValue>>;
1916
};
2017

21-
/** Intent to remove a node from its ownerDocument */
22-
export type Remove = {
23-
node: Node;
24-
};
25-
2618
/** Represents the user's intent to change an XMLDocument */
2719
export type Edit = Insert | Update | Remove | Edit[];
2820

29-
export function isComplex(edit: Edit): edit is Edit[] {
30-
return edit instanceof Array;
31-
}
32-
33-
export function isInsert(edit: Edit): edit is Insert {
34-
return (edit as Insert).parent !== undefined;
35-
}
36-
3721
export function isNamespaced(
38-
value: AttributeValue,
22+
value: unknown,
3923
): value is NamespacedAttributeValue {
40-
return value !== null && typeof value !== 'string';
24+
return (
25+
value !== null &&
26+
typeof value === 'object' &&
27+
'namespaceURI' in value &&
28+
typeof value.namespaceURI === 'string' &&
29+
'value' in value &&
30+
typeof value.value === 'string'
31+
);
4132
}
4233

43-
export function isUpdate(edit: Edit): edit is Update {
44-
return (
45-
(edit as Update).element !== undefined &&
46-
(edit as Update).attributes !== undefined
34+
export function isAttributesV1(
35+
attributes: unknown,
36+
): attributes is AttributesV1 {
37+
if (attributes === null || typeof attributes !== 'object') {
38+
return false;
39+
}
40+
41+
return Object.entries(attributes).every(
42+
([key, value]) =>
43+
typeof key === 'string' &&
44+
(value === null || typeof value === 'string' || isNamespaced(value)),
4745
);
4846
}
4947

50-
export function isRemove(edit: Edit): edit is Remove {
48+
export function isComplex(edit: unknown): edit is Edit[] {
49+
return edit instanceof Array && edit.every(isEdit);
50+
}
51+
52+
export function isUpdate(edit: unknown): edit is Update {
5153
return (
52-
(edit as Insert).parent === undefined && (edit as Remove).node !== undefined
54+
(edit as Update).element instanceof Element &&
55+
isAttributesV1((edit as Update).attributes)
5356
);
5457
}
5558

5659
export type EditEvent<E extends Edit = Edit> = CustomEvent<E>;
5760

58-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59-
export function isEdit(edit: any): edit is Edit {
61+
export function isEdit(edit: unknown): edit is Edit {
6062
if (isComplex(edit)) {
61-
return !edit.some(e => !isEdit(e));
63+
return true;
6264
}
6365

64-
return (
65-
!isSetAttributes(edit) &&
66-
(isUpdate(edit) || isInsert(edit) || isRemove(edit))
67-
);
66+
return isUpdate(edit) || isInsert(edit) || isRemove(edit);
6867
}

editv2.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,16 @@ const insert: Insert = { parent: element, node: element, reference: null };
1919
const remove: Remove = { node: element };
2020
const setAttributes: SetAttributes = {
2121
element,
22-
attributes: {},
23-
attributesNS: {},
22+
attributes: { name: 'value' },
23+
attributesNS: { namespaceURI: { name: 'value' } },
2424
};
2525
const setTextContent: SetTextContent = { element, textContent: '' };
2626

27-
describe('type guard functions for editv2', () => {
28-
it('returns false on invalid Edit type', () =>
27+
describe('isEditV2', () => {
28+
it('returns false for invalid Edit type', () =>
2929
expect('invalid edit').to.not.satisfy(isEditV2));
3030

31-
it('returns false on Update', () => expect(update).to.not.satisfy(isEditV2));
31+
it('returns false for Update', () => expect(update).to.not.satisfy(isEditV2));
3232

3333
it('returns true for Insert', () => expect(insert).to.satisfy(isEditV2));
3434

@@ -40,13 +40,13 @@ describe('type guard functions for editv2', () => {
4040
it('returns true for SetTextContent', () =>
4141
expect(setTextContent).to.satisfy(isEditV2));
4242

43-
it('returns false on mixed edit and editV2 array', () =>
43+
it('returns false for a mixed edit and editV2 array', () =>
4444
expect([update, setAttributes]).to.not.satisfy(isEditV2));
4545

46-
it('returns false on edit array', () =>
46+
it('returns false for edit array', () =>
4747
expect([update, update]).to.not.satisfy(isEditV2));
4848

49-
it('returns true on editV2 array', () =>
49+
it('returns true for editV2 array', () =>
5050
expect([setAttributes, remove, insert, setTextContent]).to.satisfy(
5151
isEditV2,
5252
));

editv2.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export type SetTextContent = {
1616
textContent: string;
1717
};
1818

19+
export type Attributes = Partial<Record<string, string | null>>;
20+
21+
export type AttributesNS = Partial<Record<string, Attributes>>;
22+
1923
/** Intent to set or remove (if `null`) `attributes`(-`NS`) on `element` */
2024
export type SetAttributes = {
2125
element: Element;
@@ -31,43 +35,69 @@ export type EditV2 =
3135
| Remove
3236
| EditV2[];
3337

34-
export function isComplex(edit: EditV2): edit is EditV2[] {
35-
return edit instanceof Array;
38+
export function isAttributes(attributes: unknown): attributes is Attributes {
39+
if (typeof attributes !== 'object' || attributes === null) {
40+
return false;
41+
}
42+
return Object.entries(attributes).every(
43+
([key, value]) =>
44+
typeof key === 'string' && (value === null || typeof value === 'string'),
45+
);
46+
}
47+
48+
export function isAttributesNS(
49+
attributesNS: unknown,
50+
): attributesNS is AttributesNS {
51+
if (typeof attributesNS !== 'object' || attributesNS === null) {
52+
return false;
53+
}
54+
return Object.entries(attributesNS).every(
55+
([namespace, attributes]) =>
56+
typeof namespace === 'string' &&
57+
isAttributes(attributes as Record<string, string | null>),
58+
);
59+
}
60+
61+
export function isComplex(edit: unknown): edit is EditV2[] {
62+
return edit instanceof Array && edit.every(e => isEditV2(e));
3663
}
3764

38-
export function isSetTextContent(edit: EditV2): edit is SetTextContent {
65+
export function isSetTextContent(edit: unknown): edit is SetTextContent {
3966
return (
40-
(edit as SetTextContent).element !== undefined &&
41-
(edit as SetTextContent).textContent !== undefined
67+
(edit as SetTextContent).element instanceof Element &&
68+
typeof (edit as SetTextContent).textContent === 'string'
4269
);
4370
}
4471

45-
export function isRemove(edit: EditV2): edit is Remove {
72+
export function isRemove(edit: unknown): edit is Remove {
4673
return (
47-
(edit as Insert).parent === undefined && (edit as Remove).node !== undefined
74+
(edit as Insert).parent === undefined &&
75+
(edit as Remove).node instanceof Node
4876
);
4977
}
5078

51-
export function isSetAttributes(edit: EditV2): edit is SetAttributes {
79+
export function isSetAttributes(edit: unknown): edit is SetAttributes {
5280
return (
53-
(edit as SetAttributes).element !== undefined &&
54-
(edit as SetAttributes).attributes !== undefined &&
55-
(edit as SetAttributes).attributesNS !== undefined
81+
(edit as SetAttributes).element instanceof Element &&
82+
isAttributes((edit as SetAttributes).attributes) &&
83+
isAttributesNS((edit as SetAttributes).attributesNS)
5684
);
5785
}
5886

59-
export function isInsert(edit: EditV2): edit is Insert {
87+
export function isInsert(edit: unknown): edit is Insert {
6088
return (
61-
(edit as Insert).parent !== undefined &&
62-
(edit as Insert).node !== undefined &&
63-
(edit as Insert).reference !== undefined
89+
((edit as Insert).parent instanceof Element ||
90+
(edit as Insert).parent instanceof Document ||
91+
(edit as Insert).parent instanceof DocumentFragment) &&
92+
(edit as Insert).node instanceof Node &&
93+
((edit as Insert).reference instanceof Node ||
94+
(edit as Insert).reference === null)
6495
);
6596
}
6697

67-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
68-
export function isEditV2(edit: any): edit is EditV2 {
98+
export function isEditV2(edit: unknown): edit is EditV2 {
6999
if (isComplex(edit)) {
70-
return !edit.some(e => !isEditV2(e));
100+
return true;
71101
}
72102

73103
return (

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
],
8787
"rules": {
8888
"no-unused-vars": "off",
89+
"no-use-before-define": "off",
8990
"class-methods-use-this": [
9091
"error",
9192
{

0 commit comments

Comments
 (0)