]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(compiler-sfc): properly handle unknown types in runtime prop inference
authorEvan You <yyx990803@gmail.com>
Tue, 28 Mar 2023 09:15:25 +0000 (17:15 +0800)
committerEvan You <yyx990803@gmail.com>
Tue, 28 Mar 2023 09:15:25 +0000 (17:15 +0800)
fix #7511

packages/compiler-sfc/__tests__/__snapshots__/compileScript.spec.ts.snap
packages/compiler-sfc/__tests__/compileScript.spec.ts
packages/compiler-sfc/src/compileScript.ts

index b7a13e568510a10767ee486fadab199543b0fef5..df4fc952bf9d2101828c0b80259ad90bf88b17c6 100644 (file)
@@ -1712,7 +1712,10 @@ export default /*#__PURE__*/_defineComponent({
     literalUnionMixed: { type: [String, Number, Boolean], required: true },
     intersection: { type: Object, required: true },
     intersection2: { type: String, required: true },
-    foo: { type: [Function, null], required: true }
+    foo: { type: [Function, null], required: true },
+    unknown: { type: null, required: true },
+    unknownUnion: { type: null, required: true },
+    unknownIntersection: { type: Object, required: true }
   },
   setup(__props: any, { expose }) {
   expose();
index 8f96ecc11c53a944b3ab8a1ed7198be70271faab..f375f51ac075b3c49ad899bea1f4196d1ec63bf4 100644 (file)
@@ -1036,6 +1036,10 @@ const emit = defineEmits(['a', 'b'])
         intersection: Test & {}
         intersection2: 'foo' & ('foo' | 'bar')
         foo: ((item: any) => boolean) | null
+
+        unknown: UnknownType
+        unknownUnion: UnknownType | string
+        unknownIntersection: UnknownType & Object
       }>()
       </script>`)
       assertCode(content)
@@ -1082,6 +1086,13 @@ const emit = defineEmits(['a', 'b'])
       expect(content).toMatch(`intersection: { type: Object, required: true }`)
       expect(content).toMatch(`intersection2: { type: String, required: true }`)
       expect(content).toMatch(`foo: { type: [Function, null], required: true }`)
+      expect(content).toMatch(`unknown: { type: null, required: true }`)
+      // uninon containing unknown type: skip check
+      expect(content).toMatch(`unknownUnion: { type: null, required: true }`)
+      // intersection containing unknown type: narrow to the known types
+      expect(content).toMatch(
+        `unknownIntersection: { type: Object, required: true }`
+      )
       expect(bindings).toStrictEqual({
         string: BindingTypes.PROPS,
         number: BindingTypes.PROPS,
@@ -1115,7 +1126,10 @@ const emit = defineEmits(['a', 'b'])
         foo: BindingTypes.PROPS,
         uppercase: BindingTypes.PROPS,
         params: BindingTypes.PROPS,
-        nonNull: BindingTypes.PROPS
+        nonNull: BindingTypes.PROPS,
+        unknown: BindingTypes.PROPS,
+        unknownUnion: BindingTypes.PROPS,
+        unknownIntersection: BindingTypes.PROPS
       })
     })
 
index e2e129924e3d68873cafefb4fbffe55ae099f0a9..47738d8285b4e4c4847185db234527c36c560f6a 100644 (file)
@@ -386,7 +386,7 @@ export function compileScript(
     isFromSetup: boolean,
     needTemplateUsageCheck: boolean
   ) {
-    // template usage check is only needed in non-inline mode, so we can skip
+    // template usage check is only needed in non-inline mode, so we can UNKNOWN
     // the work if inlineTemplate is true.
     let isUsedInTemplate = needTemplateUsageCheck
     if (
@@ -1100,7 +1100,7 @@ export function compileScript(
 
         // check if user has manually specified `name` or 'render` option in
         // export default
-        // if has name, skip name inference
+        // if has name, UNKNOWN name inference
         // if has render and no template, generate return object instead of
         // empty render function (#4980)
         let optionProperties
@@ -1403,7 +1403,7 @@ export function compileScript(
 
   // 4. extract runtime props/emits code from setup context type
   if (propsTypeDecl) {
-    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes, isProd)
+    extractRuntimeProps(propsTypeDecl, typeDeclaredProps, declaredTypes)
   }
   if (emitsTypeDecl) {
     extractRuntimeEmits(emitsTypeDecl, typeDeclaredEmits)
@@ -1576,7 +1576,7 @@ export function compileScript(
         !userImports[key].source.endsWith('.vue')
       ) {
         // generate getter for import bindings
-        // skip vue imports since we know they will never change
+        // UNKNOWN vue imports since we know they will never change
         returned += `get ${key}() { return ${key} }, `
       } else if (bindingMetadata[key] === BindingTypes.SETUP_LET) {
         // local let binding, also add setter
@@ -1972,8 +1972,7 @@ function recordType(node: Node, declaredTypes: Record<string, string[]>) {
 function extractRuntimeProps(
   node: TSTypeLiteral | TSInterfaceBody,
   props: Record<string, PropTypeData>,
-  declaredTypes: Record<string, string[]>,
-  isProd: boolean
+  declaredTypes: Record<string, string[]>
 ) {
   const members = node.type === 'TSTypeLiteral' ? node.members : node.body
   for (const m of members) {
@@ -1981,11 +1980,15 @@ function extractRuntimeProps(
       (m.type === 'TSPropertySignature' || m.type === 'TSMethodSignature') &&
       m.key.type === 'Identifier'
     ) {
-      let type
+      let type: string[] | undefined
       if (m.type === 'TSMethodSignature') {
         type = ['Function']
       } else if (m.typeAnnotation) {
         type = inferRuntimeType(m.typeAnnotation.typeAnnotation, declaredTypes)
+        // skip check for result containing unknown types
+        if (type.includes(UNKNOWN_TYPE)) {
+          type = [`null`]
+        }
       }
       props[m.key.name] = {
         key: m.key.name,
@@ -1996,6 +1999,8 @@ function extractRuntimeProps(
   }
 }
 
+const UNKNOWN_TYPE = 'Unknown'
+
 function inferRuntimeType(
   node: TSType,
   declaredTypes: Record<string, string[]>
@@ -2009,6 +2014,8 @@ function inferRuntimeType(
       return ['Boolean']
     case 'TSObjectKeyword':
       return ['Object']
+    case 'TSNullKeyword':
+      return ['null']
     case 'TSTypeLiteral': {
       // TODO (nice to have) generate runtime property validation
       const types = new Set<string>()
@@ -2041,7 +2048,7 @@ function inferRuntimeType(
         case 'BigIntLiteral':
           return ['Number']
         default:
-          return [`null`]
+          return [`UNKNOWN`]
       }
 
     case 'TSTypeReference':
@@ -2104,31 +2111,43 @@ function inferRuntimeType(
                 declaredTypes
               )
             }
-          // cannot infer, fallback to null: ThisParameterType
+          // cannot infer, fallback to UNKNOWN: ThisParameterType
         }
       }
-      return [`null`]
+      return [UNKNOWN_TYPE]
 
     case 'TSParenthesizedType':
       return inferRuntimeType(node.typeAnnotation, declaredTypes)
+
     case 'TSUnionType':
-    case 'TSIntersectionType':
-      return [
-        ...new Set(
-          [].concat(
-            ...(node.types.map(t => inferRuntimeType(t, declaredTypes)) as any)
-          )
-        )
-      ]
+      return flattenTypes(node.types, declaredTypes)
+    case 'TSIntersectionType': {
+      return flattenTypes(node.types, declaredTypes).filter(
+        t => t !== UNKNOWN_TYPE
+      )
+    }
 
     case 'TSSymbolKeyword':
       return ['Symbol']
 
     default:
-      return [`null`] // no runtime check
+      return [UNKNOWN_TYPE] // no runtime check
   }
 }
 
+function flattenTypes(
+  types: TSType[],
+  declaredTypes: Record<string, string[]>
+): string[] {
+  return [
+    ...new Set(
+      ([] as string[]).concat(
+        ...types.map(t => inferRuntimeType(t, declaredTypes))
+      )
+    )
+  ]
+}
+
 function toRuntimeTypeString(types: string[]) {
   return types.length > 1 ? `[${types.join(', ')}]` : types[0]
 }