Repository: vercel/next.js
Pull request: [ts-next-plugin] fix: warn only if no type

### Why?

The warning was still visible when variable exports for `generateMetadata` had a custom return type.

https://github.com/user-attachments/assets/cdd1f693-5373-4489-b631-55c0e56e65d8

### How?

Simply don't warn if there's an explicitly set return type.

Changed files:
filename: packages/next/src/server/typescript/rules/metadata.ts
patch: @@ -82,7 +82,7 @@ const metadata = {
 
       if (ts.isFunctionDeclaration(node)) {
         if (node.name?.getText() === 'generateMetadata') {
-          if (hasCorrectType(node)) {
+          if (hasType(node)) {
             return []
           }
 
@@ -103,7 +103,7 @@ const metadata = {
         }
       } else {
         for (const declaration of node.declarationList.declarations) {
-          if (hasCorrectType(declaration)) {
+          if (hasType(declaration)) {
             return []
           }
 
@@ -209,7 +209,7 @@ const metadata = {
             if (
               declaration &&
               ts.isFunctionDeclaration(declaration) &&
-              !hasCorrectType(declaration)
+              !hasType(declaration)
             ) {
               diagnostics.push({
                 file: source,
@@ -226,7 +226,7 @@ const metadata = {
             if (
               declaration &&
               ts.isVariableDeclaration(declaration) &&
-              !hasCorrectType(declaration)
+              !hasType(declaration)
             ) {
               diagnostics.push({
                 file: source,
@@ -246,144 +246,40 @@ const metadata = {
   },
 }
 
-function hasCorrectType(
-  node: tsModule.FunctionDeclaration | tsModule.VariableDeclaration
-): boolean {
-  // Skip if already has type.
-  if (node.type) {
-    return true
-  }
-
-  const ts = getTs()
-  const typeChecker = getTypeChecker()
-  if (!typeChecker) {
-    return false
-  }
-
-  // For generateMetadata, check if it's Promise<Metadata> for async or Metadata for sync
-  if (ts.isFunctionDeclaration(node)) {
-    return checkFunctionReturnType(node, typeChecker)
-  } else {
-    // For variable declarations (const/let/var)
-    const name = node.name.getText()
-
-    if (name === 'generateMetadata') {
-      // For generateMetadata as a variable, it must be a function expression or arrow function
-      if (
-        !node.initializer ||
-        (!ts.isFunctionExpression(node.initializer) &&
-          !ts.isArrowFunction(node.initializer))
-      ) {
-        return false
-      }
-
-      // Check the return type of the function expression/arrow function
-      if (node.initializer.type) {
-        // If it has an explicit return type annotation
-        return checkFunctionReturnType(node.initializer, typeChecker)
-      } else {
-        // If no explicit return type, infer it from the function
-        const signature = typeChecker.getSignatureFromDeclaration(
-          node.initializer
-        )
-        if (!signature) return false
-
-        const returnType = typeChecker.getReturnTypeOfSignature(signature)
-        if (!returnType) return false
-
-        const isAsync =
-          node.initializer.modifiers?.some(
-            (m) => m.kind === ts.SyntaxKind.AsyncKeyword
-          ) ?? false
-
-        if (isAsync) {
-          // For async functions, check if it's Promise<Metadata>
-          const typeSymbol = returnType.getSymbol()
-          if (!typeSymbol || typeSymbol.getName() !== 'Promise') return false
-
-          // Check if it's a reference type (like Promise<T>)
-          if (
-            !(returnType.flags & ts.TypeFlags.Object) ||
-            !('typeArguments' in returnType)
-          ) {
-            return false
-          }
-
-          const typeArgs = (
-            returnType as { typeArguments: readonly tsModule.Type[] }
-          ).typeArguments
-          if (!typeArgs || typeArgs.length !== 1) return false
-
-          const promiseType = typeArgs[0]
-          const promiseTypeSymbol = promiseType.getSymbol()
-          return promiseTypeSymbol?.getName() === 'Metadata'
-        } else {
-          // For sync functions, check if it returns Metadata
-          const returnTypeSymbol = returnType.getSymbol()
-          return returnTypeSymbol?.getName() === 'Metadata'
-        }
-      }
-    } else {
-      // For metadata export, we just need Metadata type
-      if (!node.type) return false
-      const type = typeChecker.getTypeFromTypeNode(node.type)
-      if (!type) return false
-      const symbol = type.getSymbol()
-      return symbol?.getName() === 'Metadata'
-    }
-  }
-}
-
-function checkFunctionReturnType(
+function hasType(
   node:
     | tsModule.FunctionDeclaration
+    | tsModule.VariableDeclaration
     | tsModule.FunctionExpression
-    | tsModule.ArrowFunction,
-  typeChecker: tsModule.TypeChecker
+    | tsModule.ArrowFunction
 ): boolean {
   const ts = getTs()
 
-  if (!node.type) return false
-
-  const returnType = typeChecker.getTypeFromTypeNode(node.type)
-  if (!returnType) return false
-
-  const isAsync =
-    node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false
-
-  if (isAsync) {
-    // For async functions, we need Promise<Metadata>
-    const typeSymbol = returnType.getSymbol()
-    if (!typeSymbol || typeSymbol.getName() !== 'Promise') {
-      return false
-    }
-
-    // Get the type argument of Promise<T>
-    if (!ts.isTypeReferenceNode(node.type)) {
-      return false
-    }
+  // For function declarations, expressions, and arrow functions, check if they have return type
+  if (
+    ts.isFunctionDeclaration(node) ||
+    ts.isFunctionExpression(node) ||
+    ts.isArrowFunction(node)
+  ) {
+    return !!node.type
+  }
 
-    // Check if it's a reference type (like Promise<T>)
+  // For variable declarations
+  if (!node.name) return false
+  const name = node.name.getText()
+  if (name === 'generateMetadata') {
+    // If it's a function expression or arrow function, check if it has return type
     if (
-      !(returnType.flags & ts.TypeFlags.Object) ||
-      !('typeArguments' in returnType)
+      node.initializer &&
+      (ts.isFunctionExpression(node.initializer) ||
+        ts.isArrowFunction(node.initializer))
     ) {
-      return false
-    }
-
-    const typeArgs = (returnType as { typeArguments: readonly tsModule.Type[] })
-      .typeArguments
-    if (!typeArgs || typeArgs.length !== 1) {
-      return false
+      return !!node.initializer.type
     }
-    const promiseType = typeArgs[0]
-    const promiseTypeSymbol = promiseType.getSymbol()
-    return promiseTypeSymbol?.getName() === 'Metadata'
-  } else {
-    // For sync functions, we need Metadata directly
-    const symbol = returnType.getSymbol()
-    return symbol?.getName() === 'Metadata'
   }
+
+  // For all other cases, check if the node has a type annotation
+  return !!node.type
 }
 
 export default metadata