Przeglądaj źródła

fix(custom-elements): use strict number casting

close #4946
close #2598
close #2604

This commit also refactors internal usage of previous loose
implementation of `toNumber` to the stricter version where applicable.
Use of `looseToNumber` is preserved for `v-model.number` modifier to
ensure backwards compatibility and consistency with Vue 2 behavior.
Evan You 3 lat temu
rodzic
commit
7d0c63ff43

+ 3 - 3
packages/runtime-core/src/compat/instance.ts

@@ -2,9 +2,9 @@ import {
   extend,
   extend,
   looseEqual,
   looseEqual,
   looseIndexOf,
   looseIndexOf,
+  looseToNumber,
   NOOP,
   NOOP,
-  toDisplayString,
-  toNumber
+  toDisplayString
 } from '@vue/shared'
 } from '@vue/shared'
 import {
 import {
   ComponentPublicInstance,
   ComponentPublicInstance,
@@ -148,7 +148,7 @@ export function installCompatInstanceProperties(map: PublicPropertiesMap) {
       $createElement: () => compatH,
       $createElement: () => compatH,
       _c: () => compatH,
       _c: () => compatH,
       _o: () => legacyMarkOnce,
       _o: () => legacyMarkOnce,
-      _n: () => toNumber,
+      _n: () => looseToNumber,
       _s: () => toDisplayString,
       _s: () => toDisplayString,
       _l: () => renderList,
       _l: () => renderList,
       _t: i => legacyRenderSlot.bind(null, i),
       _t: i => legacyRenderSlot.bind(null, i),

+ 3 - 3
packages/runtime-core/src/componentEmits.ts

@@ -10,8 +10,8 @@ import {
   isObject,
   isObject,
   isString,
   isString,
   isOn,
   isOn,
-  toNumber,
-  UnionToIntersection
+  UnionToIntersection,
+  looseToNumber
 } from '@vue/shared'
 } from '@vue/shared'
 import {
 import {
   ComponentInternalInstance,
   ComponentInternalInstance,
@@ -126,7 +126,7 @@ export function emit(
       args = rawArgs.map(a => (isString(a) ? a.trim() : a))
       args = rawArgs.map(a => (isString(a) ? a.trim() : a))
     }
     }
     if (number) {
     if (number) {
-      args = rawArgs.map(toNumber)
+      args = rawArgs.map(looseToNumber)
     }
     }
   }
   }
 
 

+ 10 - 1
packages/runtime-core/src/components/Suspense.ts

@@ -22,7 +22,12 @@ import {
 } from '../renderer'
 } from '../renderer'
 import { queuePostFlushCb } from '../scheduler'
 import { queuePostFlushCb } from '../scheduler'
 import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
 import { filterSingleRoot, updateHOCHostEl } from '../componentRenderUtils'
-import { pushWarningContext, popWarningContext, warn } from '../warning'
+import {
+  pushWarningContext,
+  popWarningContext,
+  warn,
+  assertNumber
+} from '../warning'
 import { handleError, ErrorCodes } from '../errorHandling'
 import { handleError, ErrorCodes } from '../errorHandling'
 
 
 export interface SuspenseProps {
 export interface SuspenseProps {
@@ -419,6 +424,10 @@ function createSuspenseBoundary(
   } = rendererInternals
   } = rendererInternals
 
 
   const timeout = toNumber(vnode.props && vnode.props.timeout)
   const timeout = toNumber(vnode.props && vnode.props.timeout)
+  if (__DEV__) {
+    assertNumber(timeout, `Suspense timeout`)
+  }
+
   const suspense: SuspenseBoundary = {
   const suspense: SuspenseBoundary = {
     vnode,
     vnode,
     parent,
     parent,

+ 1 - 1
packages/runtime-core/src/index.ts

@@ -104,7 +104,7 @@ export { useSSRContext, ssrContextKey } from './helpers/useSsrContext'
 
 
 export { createRenderer, createHydrationRenderer } from './renderer'
 export { createRenderer, createHydrationRenderer } from './renderer'
 export { queuePostFlushCb } from './scheduler'
 export { queuePostFlushCb } from './scheduler'
-export { warn } from './warning'
+export { warn, assertNumber } from './warning'
 export {
 export {
   handleError,
   handleError,
   callWithErrorHandling,
   callWithErrorHandling,

+ 12 - 0
packages/runtime-core/src/warning.ts

@@ -162,3 +162,15 @@ function formatProp(key: string, value: unknown, raw?: boolean): any {
     return raw ? value : [`${key}=`, value]
     return raw ? value : [`${key}=`, value]
   }
   }
 }
 }
+
+/**
+ * @internal
+ */
+export function assertNumber(val: unknown, type: string) {
+  if (!__DEV__) return
+  if (typeof val !== 'number') {
+    warn(`${type} is not a valid number - ` + `got ${JSON.stringify(val)}.`)
+  } else if (isNaN(val)) {
+    warn(`${type} is NaN - ` + 'the duration expression might be incorrect.')
+  }
+}

+ 2 - 16
packages/runtime-dom/src/components/Transition.ts

@@ -2,7 +2,7 @@ import {
   BaseTransition,
   BaseTransition,
   BaseTransitionProps,
   BaseTransitionProps,
   h,
   h,
-  warn,
+  assertNumber,
   FunctionalComponent,
   FunctionalComponent,
   compatUtils,
   compatUtils,
   DeprecationTypes
   DeprecationTypes
@@ -283,24 +283,10 @@ function normalizeDuration(
 
 
 function NumberOf(val: unknown): number {
 function NumberOf(val: unknown): number {
   const res = toNumber(val)
   const res = toNumber(val)
-  if (__DEV__) validateDuration(res)
+  if (__DEV__) assertNumber(res, '<transition> explicit duration')
   return res
   return res
 }
 }
 
 
-function validateDuration(val: unknown) {
-  if (typeof val !== 'number') {
-    warn(
-      `<transition> explicit duration is not a valid number - ` +
-        `got ${JSON.stringify(val)}.`
-    )
-  } else if (isNaN(val)) {
-    warn(
-      `<transition> explicit duration is NaN - ` +
-        'the duration expression might be incorrect.'
-    )
-  }
-}
-
 export function addTransitionClass(el: Element, cls: string) {
 export function addTransitionClass(el: Element, cls: string) {
   cls.split(/\s+/).forEach(c => c && el.classList.add(c))
   cls.split(/\s+/).forEach(c => c && el.classList.add(c))
   ;(
   ;(

+ 7 - 4
packages/runtime-dom/src/directives/vModel.ts

@@ -11,7 +11,7 @@ import {
   looseEqual,
   looseEqual,
   looseIndexOf,
   looseIndexOf,
   invokeArrayFns,
   invokeArrayFns,
-  toNumber,
+  looseToNumber,
   isSet
   isSet
 } from '@vue/shared'
 } from '@vue/shared'
 
 
@@ -54,7 +54,7 @@ export const vModelText: ModelDirective<
         domValue = domValue.trim()
         domValue = domValue.trim()
       }
       }
       if (castToNumber) {
       if (castToNumber) {
-        domValue = toNumber(domValue)
+        domValue = looseToNumber(domValue)
       }
       }
       el._assign(domValue)
       el._assign(domValue)
     })
     })
@@ -88,7 +88,10 @@ export const vModelText: ModelDirective<
       if (trim && el.value.trim() === value) {
       if (trim && el.value.trim() === value) {
         return
         return
       }
       }
-      if ((number || el.type === 'number') && toNumber(el.value) === value) {
+      if (
+        (number || el.type === 'number') &&
+        looseToNumber(el.value) === value
+      ) {
         return
         return
       }
       }
     }
     }
@@ -182,7 +185,7 @@ export const vModelSelect: ModelDirective<HTMLSelectElement> = {
       const selectedVal = Array.prototype.filter
       const selectedVal = Array.prototype.filter
         .call(el.options, (o: HTMLOptionElement) => o.selected)
         .call(el.options, (o: HTMLOptionElement) => o.selected)
         .map((o: HTMLOptionElement) =>
         .map((o: HTMLOptionElement) =>
-          number ? toNumber(getValue(o)) : getValue(o)
+          number ? looseToNumber(getValue(o)) : getValue(o)
         )
         )
       el._assign(
       el._assign(
         el.multiple
         el.multiple

+ 13 - 1
packages/shared/src/index.ts

@@ -153,11 +153,23 @@ export const def = (obj: object, key: string | symbol, value: any) => {
   })
   })
 }
 }
 
 
-export const toNumber = (val: any): any => {
+/**
+ * "123-foo" will be parsed to 123
+ * This is used for the .number modifier in v-model
+ */
+export const looseToNumber = (val: any): any => {
   const n = parseFloat(val)
   const n = parseFloat(val)
   return isNaN(n) ? val : n
   return isNaN(n) ? val : n
 }
 }
 
 
+/**
+ * "123-foo" will be returned as-is
+ */
+export const toNumber = (val: any): any => {
+  const n = Number(val)
+  return isNaN(n) ? val : n
+}
+
 let _globalThis: any
 let _globalThis: any
 export const getGlobalThis = (): any => {
 export const getGlobalThis = (): any => {
   return (
   return (