Skip to content

Commit

Permalink
fix(types): properly type check unbound array methods @W-15336947 (#4101
Browse files Browse the repository at this point in the history
)

* fix: use more accurate type for Array#every

* feat(types): use better types for unbound array methods

* fix: update function signature instead of implementation
  • Loading branch information
wjhsf committed Apr 12, 2024
1 parent 4cac8a2 commit 1dba707
Show file tree
Hide file tree
Showing 17 changed files with 204 additions and 108 deletions.
15 changes: 9 additions & 6 deletions packages/@lwc/engine-core/src/framework/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -39,6 +39,7 @@ import {
isVScopedSlotFragment,
isVStatic,
Key,
MutableVNodes,
VComment,
VCustomElement,
VElement,
Expand Down Expand Up @@ -379,7 +380,7 @@ function i(
iterable: Iterable<any>,
factory: (value: any, index: number, first: boolean, last: boolean) => VNodes | VNode
): VNodes {
const list: VNodes = [];
const list: MutableVNodes = [];
// TODO [#1276]: compiler should give us some sort of indicator when a vnodes collection is dynamic
sc(list);
const vmBeingRendered = getVMBeingRendered()!;
Expand Down Expand Up @@ -431,7 +432,8 @@ function i(
if (isArray(vnode)) {
ArrayPush.apply(list, vnode);
} else {
ArrayPush.call(list, vnode);
// `isArray` doesn't narrow this block properly...
ArrayPush.call(list, vnode as VNode | null);
}

if (process.env.NODE_ENV !== 'production') {
Expand Down Expand Up @@ -470,20 +472,21 @@ function i(
* [f]lattening
* @param items
*/
function f(items: Readonly<Array<Readonly<Array<VNodes>> | VNodes>>): VNodes {
function f(items: ReadonlyArray<VNodes> | VNodes): VNodes {
if (process.env.NODE_ENV !== 'production') {
assert.isTrue(isArray(items), 'flattening api can only work with arrays.');
}
const len = items.length;
const flattened: VNodes = [];
const flattened: MutableVNodes = [];
// TODO [#1276]: compiler should give us some sort of indicator when a vnodes collection is dynamic
sc(flattened);
for (let j = 0; j < len; j += 1) {
const item = items[j];
if (isArray(item)) {
ArrayPush.apply(flattened, item);
} else {
ArrayPush.call(flattened, item);
// `isArray` doesn't narrow this block properly...
ArrayPush.call(flattened, item as VNode | null);
}
}
return flattened;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -68,7 +68,7 @@ function createMethodCaller(methodName: string): (...args: any[]) => any {
const vm = getAssociatedVM(this);
const { callHook, component } = vm;
const fn = (component as any)[methodName];
return callHook(vm.component, fn, ArraySlice.call(arguments));
return callHook(vm.component, fn, ArraySlice.call(arguments as unknown as unknown[]));
};
}

Expand Down
8 changes: 4 additions & 4 deletions packages/@lwc/engine-core/src/framework/freeze-template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -116,10 +116,10 @@ function warnOnArrayMutation(stylesheets: TemplateStylesheetFactories) {
// we can at least warn when they use the most common mutation methods.
for (const prop of ARRAY_MUTATION_METHODS) {
const originalArrayMethod = getOriginalArrayMethod(prop);
stylesheets[prop] = function arrayMutationWarningWrapper() {
// Assertions used here because TypeScript can't handle mapping over our types
(stylesheets as any)[prop] = function arrayMutationWarningWrapper() {
reportTemplateViolation('stylesheets');
// @ts-expect-error can't properly determine the right `this`
return originalArrayMethod.apply(this, arguments);
return originalArrayMethod.apply(this, arguments as any);
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/@lwc/engine-core/src/framework/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ function getValidationPredicate(
// If validationOptOut is an array of strings, attributes specified in the
// array will be "opted out". Attributes not specified in the array will still
// be validated.
if (isArray(optOutStaticProp) && arrayEvery<string>(optOutStaticProp, isString)) {
if (isArray(optOutStaticProp) && arrayEvery(optOutStaticProp, isString)) {
return (attrName: string) => !ArrayIncludes.call(optOutStaticProp, attrName);
}
if (process.env.NODE_ENV !== 'production') {
Expand Down
11 changes: 6 additions & 5 deletions packages/@lwc/engine-core/src/framework/rendering.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -49,6 +49,7 @@ import {
isVScopedSlotFragment,
isVStatic,
Key,
MutableVNodes,
VBaseElement,
VComment,
VCustomElement,
Expand Down Expand Up @@ -717,11 +718,11 @@ export function allocateChildren(vnode: VCustomElement, vm: VM) {
* @param children
*/
function flattenFragmentsInChildren(children: VNodes): VNodes {
const flattenedChildren: VNodes = [];
const flattenedChildren: MutableVNodes = [];

// Initialize our stack with the direct children of the custom component and check whether we have a VFragment.
// If no VFragment is found in children, we don't need to traverse anything or mark the children dynamic and can return early.
const nodeStack: VNodes = [];
const nodeStack: MutableVNodes = [];
let fragmentFound = false;
for (let i = children.length - 1; i > -1; i -= 1) {
const child = children[i];
Expand Down Expand Up @@ -780,7 +781,7 @@ function createViewModelHook(elm: HTMLElement, vnode: VCustomElement, renderer:
return vm;
}

function allocateInSlot(vm: VM, children: VNodes, owner: VM) {
function allocateInSlot(vm: VM, children: VNodes, owner: VM): void {
const {
cmpSlots: { slotAssignments: oldSlotsMapping },
} = vm;
Expand All @@ -807,7 +808,7 @@ function allocateInSlot(vm: VM, children: VNodes, owner: VM) {
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const normalizedSlotName = '' + slotName;

const vnodes: VNodes = (cmpSlotsMapping[normalizedSlotName] =
const vnodes: MutableVNodes = (cmpSlotsMapping[normalizedSlotName] =
cmpSlotsMapping[normalizedSlotName] || []);
ArrayPush.call(vnodes, vnode);
}
Expand Down
14 changes: 10 additions & 4 deletions packages/@lwc/engine-core/src/framework/template.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -44,7 +44,7 @@ import {
} from './stylesheet';
import { logOperationEnd, logOperationStart, OperationId } from './profiler';
import { getTemplateOrSwappedTemplate, setActiveVM } from './hot-swaps';
import { VNodes, VStaticPart, VStaticPartElement, VStaticPartText } from './vnodes';
import { MutableVNodes, VNodes, VStaticPart, VStaticPartElement, VStaticPartText } from './vnodes';
import { RendererAPI } from './renderer';
import { getMapFromClassName } from './modules/computed-class-attr';

Expand Down Expand Up @@ -370,7 +370,7 @@ export function evaluateTemplate(vm: VM, html: Template): VNodes {
}
const isUpdatingTemplateInception = isUpdatingTemplate;
const vmOfTemplateBeingUpdatedInception = vmBeingRendered;
let vnodes: VNodes = [];
let vnodes: MutableVNodes = [];

runWithBoundaryProtection(
vm,
Expand Down Expand Up @@ -446,7 +446,13 @@ export function evaluateTemplate(vm: VM, html: Template): VNodes {
// Set the global flag that template is being updated
isUpdatingTemplate = true;

vnodes = html.call(undefined, api, component, cmpSlots, context.tplCache);
vnodes = html.call(
undefined,
api,
component,
cmpSlots,
context.tplCache
) as MutableVNodes;
const { styleVNodes } = context;
if (!isNull(styleVNodes)) {
ArrayUnshift.apply(vnodes, styleVNodes);
Expand Down
11 changes: 8 additions & 3 deletions packages/@lwc/engine-core/src/framework/vnodes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -34,7 +34,12 @@ export type VNode =
| VFragment
| VScopedSlotFragment;

export type VNodes = Readonly<Array<VNode | null>>;
export type VNodes = ReadonlyArray<VNode | null>;
/**
* Mutable version of {@link VNodes}. It should only be used inside functions to build an array;
* it should never be used as a parameter or return type.
*/
export type MutableVNodes = Array<VNode | null>;

export interface BaseVParent {
children: VNodes;
Expand Down Expand Up @@ -144,7 +149,7 @@ export interface VNodeData {
readonly className?: string;
readonly style?: string;
readonly classMap?: Readonly<Record<string, boolean>>;
readonly styleDecls?: Readonly<Array<[string, string, boolean]>>;
readonly styleDecls?: ReadonlyArray<[string, string, boolean]>;
readonly context?: Readonly<Record<string, Readonly<Record<string, any>>>>;
readonly on?: Readonly<Record<string, (event: Event) => any>>;
readonly svg?: boolean;
Expand Down
7 changes: 4 additions & 3 deletions packages/@lwc/engine-core/src/framework/wiring/wiring.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -246,8 +246,9 @@ export function installWireAdapters(vm: VM) {
vm.debugInfo![WIRE_DEBUG_ENTRY] = create(null);
}

const wiredConnecting = (context.wiredConnecting = []);
const wiredDisconnecting = (context.wiredDisconnecting = []);
const wiredConnecting: VM['context']['wiredConnecting'] = (context.wiredConnecting = []);
const wiredDisconnecting: VM['context']['wiredDisconnecting'] = (context.wiredDisconnecting =
[]);

for (const fieldNameOrMethod in wire) {
const descriptor = wire[fieldNameOrMethod];
Expand Down
53 changes: 48 additions & 5 deletions packages/@lwc/shared/src/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,49 @@ const {
/** Detached {@linkcode Array.isArray}; see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray MDN Reference}. */
const { isArray } = Array;

/** The most extensible array type. */
type BaseArray = readonly unknown[];
/** Names of methods that can be used on a readonly array. */
type ArrayPureMethodNames = {
[K in keyof BaseArray]: K extends string
? BaseArray[K] extends (...args: any) => any
? K
: never
: never;
}[keyof BaseArray];
/**
* Unbound array methods, re-typed so that `.call` and `.apply` correctly report type errors.
* @example
* const arr = ['a', 'b', 'c']
* const trim = (str: string) => str.trim()
* const sq = (num: number) => num ** 2
* const unboundForEach = arr.forEach
* unboundForEach.call(arr, trim) // passes - good
* unboundForEach.call(arr, sq) // passes - BAD!
* const fixedForEach = arr.forEach as UnboundArrayPureMethods['forEach']
* fixedForEach.call(arr, trim) // passes - good
* fixedForEach.call(arr, sq) // error - yay!
*/
type UnboundArrayPureMethods = {
[K in ArrayPureMethodNames]: {
call: <T extends BaseArray>(thisArg: T, ...args: Parameters<T[K]>) => ReturnType<T[K]>;
apply: <T extends BaseArray>(thisArg: T, args: Parameters<T[K]>) => ReturnType<T[K]>;
};
};

/** Names of methods that mutate an array (cannot be used on a readonly array). */
type ArrayMutationMethodNames = Exclude<keyof unknown[], keyof BaseArray>;
/**
* Unbound array mutation methods, re-typed so that `.call` and `.apply` correctly report type errors.
* @see {@link UnboundArrayPureMethods} for an example showing why this is needed.
*/
type UnboundArrayMutationMethods = {
[K in ArrayMutationMethodNames]: {
call: <T extends unknown[]>(thisArg: T, ...args: Parameters<T[K]>) => ReturnType<T[K]>;
apply: <T extends unknown[]>(thisArg: T, args: Parameters<T[K]>) => ReturnType<T[K]>;
};
};

// For some reason, JSDoc don't get picked up for multiple renamed destructured constants (even
// though it works fine for one, e.g. isArray), so comments for these are added to the export
// statement, rather than this declaration.
Expand All @@ -67,7 +110,7 @@ const {
splice: ArraySplice,
unshift: ArrayUnshift,
forEach, // Weird anomaly!
} = Array.prototype;
}: UnboundArrayPureMethods & UnboundArrayMutationMethods = Array.prototype;

// The type of the return value of Array.prototype.every is `this is T[]`. However, once this
// Array method is pulled out of the prototype, the function is now referencing `this` where
Expand All @@ -82,10 +125,10 @@ const {
* @param predicate A function to execute for each element of the array.
* @returns Whether all elements in the array pass the test provided by the predicate.
*/
function arrayEvery<T>(
arr: unknown[],
predicate: (value: any, index: number, array: typeof arr) => value is T
): arr is T[] {
function arrayEvery<S extends T, T = unknown>(
arr: readonly T[],
predicate: (value: any, index: number, array: readonly T[]) => value is S
): arr is readonly S[] {
return ArrayEvery.call(arr, predicate);
}

Expand Down
26 changes: 19 additions & 7 deletions packages/@lwc/synthetic-shadow/src/faux-shadow/element.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* Copyright (c) 2024, Salesforce, Inc.
* All rights reserved.
* SPDX-License-Identifier: MIT
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT
Expand Down Expand Up @@ -98,7 +98,7 @@ function childrenGetterPatched(this: Element): HTMLCollectionOf<Element> {
? filteredChildNodes
: getAllMatches(owner, filteredChildNodes);
return createStaticHTMLCollection(
ArrayFilter.call(childNodes, (node: Node | Element) => node instanceof Element)
ArrayFilter.call(childNodes, (node) => node instanceof Element) as Element[]
);
}

Expand Down Expand Up @@ -235,7 +235,10 @@ if (hasOwnProperty.call(HTMLElement.prototype, 'children')) {

function querySelectorPatched(this: Element /*, selector: string*/): Element | null {
const nodeList = arrayFromCollection(
elementQuerySelectorAll.apply(this, ArraySlice.call(arguments) as [string])
elementQuerySelectorAll.apply(
this,
ArraySlice.call(arguments as unknown as unknown[]) as [string]
)
);
if (isSyntheticShadowHost(this)) {
// element with shadowRoot attached
Expand Down Expand Up @@ -340,7 +343,10 @@ defineProperties(Element.prototype, {
querySelectorAll: {
value(this: HTMLBodyElement): NodeListOf<Element> {
const nodeList = arrayFromCollection(
elementQuerySelectorAll.apply(this, ArraySlice.call(arguments) as [string])
elementQuerySelectorAll.apply(
this,
ArraySlice.call(arguments as unknown as unknown[]) as [string]
)
);

// Note: we deviate from native shadow here, but are not fixing
Expand All @@ -362,7 +368,7 @@ if (process.env.NODE_ENV !== 'test') {
const elements = arrayFromCollection(
elementGetElementsByClassName.apply(
this,
ArraySlice.call(arguments) as [string]
ArraySlice.call(arguments as unknown as unknown[]) as [string]
)
);

Expand All @@ -379,7 +385,10 @@ if (process.env.NODE_ENV !== 'test') {
getElementsByTagName: {
value(this: HTMLBodyElement): HTMLCollectionOf<Element> {
const elements = arrayFromCollection(
elementGetElementsByTagName.apply(this, ArraySlice.call(arguments) as [string])
elementGetElementsByTagName.apply(
this,
ArraySlice.call(arguments as unknown as unknown[]) as [tagName: string]
)
);

// Note: we deviate from native shadow here, but are not fixing
Expand All @@ -397,7 +406,10 @@ if (process.env.NODE_ENV !== 'test') {
const elements = arrayFromCollection(
elementGetElementsByTagNameNS.apply(
this,
ArraySlice.call(arguments) as [string, string]
ArraySlice.call(arguments as unknown as unknown[]) as [
namespace: string,
localName: string
]
)
);

Expand Down

0 comments on commit 1dba707

Please sign in to comment.