diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 06bdf81dd43dc..88a1a62ce4cd2 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -251,6 +251,15 @@ namespace ts { case SyntaxKind.FunctionDeclaration: case SyntaxKind.ClassDeclaration: return node.flags & NodeFlags.Default ? "default" : undefined; + case SyntaxKind.JSDocFunctionType: + return isJSDocConstructSignature(node) ? "__new" : "__call"; + case SyntaxKind.Parameter: + // Parameters with names are handled at the top of this function. Parameters + // without names can only come from JSDocFunctionTypes. + Debug.assert(node.parent.kind === SyntaxKind.JSDocFunctionType); + let functionType = node.parent; + let index = indexOf(functionType.parameters, node); + return "p" + index; } } @@ -421,7 +430,6 @@ namespace ts { addToContainerChain(container); } - else if (containerFlags & ContainerFlags.IsBlockScopedContainer) { blockScopeContainer = node; blockScopeContainer.locals = undefined; @@ -459,6 +467,10 @@ namespace ts { labelStack = labelIndexMap = implicitLabels = undefined; } + if (isInJavaScriptFile(node) && node.jsDocComment) { + bind(node.jsDocComment); + } + bindReachableStatement(node); if (currentReachabilityState === Reachability.Reachable && isFunctionLikeKind(kind) && nodeIsPresent((node).body)) { @@ -722,8 +734,9 @@ namespace ts { case SyntaxKind.ClassDeclaration: case SyntaxKind.InterfaceDeclaration: case SyntaxKind.EnumDeclaration: - case SyntaxKind.TypeLiteral: case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocRecordType: return ContainerFlags.IsContainer; case SyntaxKind.CallSignature: @@ -809,6 +822,7 @@ namespace ts { case SyntaxKind.TypeLiteral: case SyntaxKind.ObjectLiteralExpression: case SyntaxKind.InterfaceDeclaration: + case SyntaxKind.JSDocRecordType: // Interface/Object-types always have their children added to the 'members' of // their container. They are only accessible through an instance of their // container, and are never in scope otherwise (even inside the body of the @@ -829,6 +843,7 @@ namespace ts { case SyntaxKind.FunctionDeclaration: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: + case SyntaxKind.JSDocFunctionType: case SyntaxKind.TypeAliasDeclaration: // All the children of these container types are never visible through another // symbol (i.e. through another symbol's 'exports' or 'members'). Instead, @@ -910,7 +925,7 @@ namespace ts { } } - function bindFunctionOrConstructorType(node: SignatureDeclaration) { + function bindFunctionOrConstructorType(node: SignatureDeclaration): void { // For a given function symbol "<...>(...) => T" we want to generate a symbol identical // to the one we would get for: { <...>(...): T } // @@ -985,7 +1000,7 @@ namespace ts { declareModuleMember(node, symbolFlags, symbolExcludes); break; } - // fall through. + // fall through. default: if (!blockScopeContainer.locals) { blockScopeContainer.locals = {}; @@ -1264,12 +1279,14 @@ namespace ts { return bindVariableDeclarationOrBindingElement(node); case SyntaxKind.PropertyDeclaration: case SyntaxKind.PropertySignature: + case SyntaxKind.JSDocRecordMember: return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property | ((node).questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes); case SyntaxKind.PropertyAssignment: case SyntaxKind.ShorthandPropertyAssignment: return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes); case SyntaxKind.EnumMember: return bindPropertyOrMethodOrAccessor(node, SymbolFlags.EnumMember, SymbolFlags.EnumMemberExcludes); + case SyntaxKind.CallSignature: case SyntaxKind.ConstructSignature: case SyntaxKind.IndexSignature: @@ -1292,8 +1309,10 @@ namespace ts { return bindPropertyOrMethodOrAccessor(node, SymbolFlags.SetAccessor, SymbolFlags.SetAccessorExcludes); case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: + case SyntaxKind.JSDocFunctionType: return bindFunctionOrConstructorType(node); case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocRecordType: return bindAnonymousDeclaration(node, SymbolFlags.TypeLiteral, "__type"); case SyntaxKind.ObjectLiteralExpression: return bindObjectLiteralExpression(node); diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b3f1cfe6c74cc..4ea8c71e0548a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -557,7 +557,9 @@ namespace ts { // - Type parameters of a function are in scope in the entire function declaration, including the parameter // list and return type. However, local types are only in scope in the function body. // - parameters are only in the scope of function body - if (meaning & result.flags & SymbolFlags.Type) { + // This restriction does not apply to JSDoc comment types because they are parented + // at a higher level than type parameters would normally be + if (meaning & result.flags & SymbolFlags.Type && lastLocation.kind !== SyntaxKind.JSDocComment) { useResult = result.flags & SymbolFlags.TypeParameter // type parameters are visible in parameter list, return type and type parameter list ? lastLocation === (location).type || @@ -2591,8 +2593,54 @@ namespace ts { return type; } + function getTypeForVariableLikeDeclarationFromJSDocComment(declaration: VariableLikeDeclaration) { + const jsDocType = getJSDocTypeForVariableLikeDeclarationFromJSDocComment(declaration); + if (jsDocType) { + return getTypeFromTypeNode(jsDocType); + } + } + + function getJSDocTypeForVariableLikeDeclarationFromJSDocComment(declaration: VariableLikeDeclaration): JSDocType { + // First, see if this node has an @type annotation on it directly. + const typeTag = getJSDocTypeTag(declaration); + if (typeTag) { + return typeTag.typeExpression.type; + } + + if (declaration.kind === SyntaxKind.VariableDeclaration && + declaration.parent.kind === SyntaxKind.VariableDeclarationList && + declaration.parent.parent.kind === SyntaxKind.VariableStatement) { + + // @type annotation might have been on the variable statement, try that instead. + const annotation = getJSDocTypeTag(declaration.parent.parent); + if (annotation) { + return annotation.typeExpression.type; + } + } + else if (declaration.kind === SyntaxKind.Parameter) { + // If it's a parameter, see if the parent has a jsdoc comment with an @param + // annotation. + const paramTag = getCorrespondingJSDocParameterTag(declaration); + if (paramTag && paramTag.typeExpression) { + return paramTag.typeExpression.type; + } + } + + return undefined; + } + // Return the inferred type for a variable, parameter, or property declaration function getTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration): Type { + if (declaration.parserContextFlags & ParserContextFlags.JavaScriptFile) { + // If this is a variable in a JavaScript file, then use the JSDoc type (if it has + // one as its type), otherwise fallback to the below standard TS codepaths to + // try to figure it out. + const type = getTypeForVariableLikeDeclarationFromJSDocComment(declaration); + if (type && type !== unknownType) { + return type; + } + } + // A variable declared in a for..in statement is always of type string if (declaration.parent.parent.kind === SyntaxKind.ForInStatement) { return stringType; @@ -3910,6 +3958,17 @@ namespace ts { return getIndexTypeOfStructuredType(getApparentType(type), kind); } + function getTypeParametersFromJSDocTemplate(declaration: SignatureDeclaration): TypeParameter[] { + if (declaration.parserContextFlags & ParserContextFlags.JavaScriptFile) { + const templateTag = getJSDocTemplateTag(declaration); + if (templateTag) { + return getTypeParametersFromDeclaration(templateTag.typeParameters); + } + } + + return undefined; + } + // Return list of type parameters with duplicates removed (duplicate identifier errors are generated in the actual // type checking functions). function getTypeParametersFromDeclaration(typeParameterDeclarations: TypeParameterDeclaration[]): TypeParameter[] { @@ -3934,6 +3993,23 @@ namespace ts { } function isOptionalParameter(node: ParameterDeclaration) { + if (node.parserContextFlags & ParserContextFlags.JavaScriptFile) { + if (node.type && node.type.kind === SyntaxKind.JSDocOptionalType) { + return true; + } + + const paramTag = getCorrespondingJSDocParameterTag(node); + if (paramTag) { + if (paramTag.isBracketed) { + return true; + } + + if (paramTag.typeExpression) { + return paramTag.typeExpression.type.kind === SyntaxKind.JSDocOptionalType; + } + } + } + if (hasQuestionToken(node)) { return true; } @@ -3974,12 +4050,20 @@ namespace ts { getDeclaredTypeOfClassOrInterface(getMergedSymbol((declaration.parent).symbol)) : undefined; const typeParameters = classType ? classType.localTypeParameters : - declaration.typeParameters ? getTypeParametersFromDeclaration(declaration.typeParameters) : undefined; + declaration.typeParameters ? getTypeParametersFromDeclaration(declaration.typeParameters) : + getTypeParametersFromJSDocTemplate(declaration); const parameters: Symbol[] = []; let hasStringLiterals = false; let minArgumentCount = -1; - for (let i = 0, n = declaration.parameters.length; i < n; i++) { + const isJSConstructSignature = isJSDocConstructSignature(declaration); + let returnType: Type = undefined; + + // If this is a JSDoc construct signature, then skip the first parameter in the + // parameter list. The first parameter represents the return type of the construct + // signature. + for (let i = isJSConstructSignature ? 1 : 0, n = declaration.parameters.length; i < n; i++) { const param = declaration.parameters[i]; + let paramSymbol = param.symbol; // Include parameter symbol instead of property symbol in the signature if (paramSymbol && !!(paramSymbol.flags & SymbolFlags.Property) && !isBindingPattern(param.name)) { @@ -3987,6 +4071,7 @@ namespace ts { paramSymbol = resolvedSymbol; } parameters.push(paramSymbol); + if (param.type && param.type.kind === SyntaxKind.StringLiteralType) { hasStringLiterals = true; } @@ -4006,14 +4091,24 @@ namespace ts { minArgumentCount = declaration.parameters.length; } - let returnType: Type; - if (classType) { + if (isJSConstructSignature) { + minArgumentCount--; + returnType = getTypeFromTypeNode(declaration.parameters[0].type); + } + else if (classType) { returnType = classType; } else if (declaration.type) { returnType = getTypeFromTypeNode(declaration.type); } else { + if (declaration.parserContextFlags & ParserContextFlags.JavaScriptFile) { + const type = getReturnTypeFromJSDocComment(declaration); + if (type && type !== unknownType) { + returnType = type; + } + } + // TypeScript 1.0 spec (April 2014): // If only one accessor includes a type annotation, the other behaves as if it had the same type annotation. if (declaration.kind === SyntaxKind.GetAccessor && !hasDynamicName(declaration)) { @@ -4050,6 +4145,7 @@ namespace ts { case SyntaxKind.SetAccessor: case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: + case SyntaxKind.JSDocFunctionType: // Don't include signature if node is the implementation of an overloaded function. A node is considered // an implementation node if it has a body and the previous node is of the same kind and immediately // precedes the implementation node (i.e. has the same parent and ends where the implementation starts). @@ -4269,7 +4365,7 @@ namespace ts { } // Get type from reference to class or interface - function getTypeFromClassOrInterfaceReference(node: TypeReferenceNode | ExpressionWithTypeArguments, symbol: Symbol): Type { + function getTypeFromClassOrInterfaceReference(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference, symbol: Symbol): Type { const type = getDeclaredTypeOfSymbol(symbol); const typeParameters = type.localTypeParameters; if (typeParameters) { @@ -4292,7 +4388,7 @@ namespace ts { // Get type from reference to type alias. When a type alias is generic, the declared type of the type alias may include // references to the type parameters of the alias. We replace those with the actual type arguments by instantiating the // declared type. Instantiations are cached using the type identities of the type arguments as the key. - function getTypeFromTypeAliasReference(node: TypeReferenceNode | ExpressionWithTypeArguments, symbol: Symbol): Type { + function getTypeFromTypeAliasReference(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference, symbol: Symbol): Type { const type = getDeclaredTypeOfSymbol(symbol); const links = getSymbolLinks(symbol); const typeParameters = links.typeParameters; @@ -4313,7 +4409,7 @@ namespace ts { } // Get type from reference to named type that cannot be generic (enum or type parameter) - function getTypeFromNonGenericTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments, symbol: Symbol): Type { + function getTypeFromNonGenericTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference, symbol: Symbol): Type { if (node.typeArguments) { error(node, Diagnostics.Type_0_is_not_generic, symbolToString(symbol)); return unknownType; @@ -4321,18 +4417,83 @@ namespace ts { return getDeclaredTypeOfSymbol(symbol); } - function getTypeFromTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments): Type { + function getTypeReferenceName(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference): LeftHandSideExpression | EntityName { + switch (node.kind) { + case SyntaxKind.TypeReference: + return (node).typeName; + case SyntaxKind.JSDocTypeReference: + return (node).name; + case SyntaxKind.ExpressionWithTypeArguments: + // We only support expressions that are simple qualified names. For other + // expressions this produces undefined. + if (isSupportedExpressionWithTypeArguments(node)) { + return (node).expression; + } + + // fall through; + } + + return undefined; + } + + function resolveTypeReferenceName( + node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference, + typeReferenceName: LeftHandSideExpression | EntityName) { + + if (!typeReferenceName) { + return unknownSymbol; + } + + return resolveEntityName(typeReferenceName, SymbolFlags.Type) || unknownSymbol; + } + + function getTypeReferenceType(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference, symbol: Symbol) { + if (symbol === unknownSymbol) { + return unknownType; + } + + if (symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface)) { + return getTypeFromClassOrInterfaceReference(node, symbol); + } + + if (symbol.flags & SymbolFlags.TypeAlias) { + return getTypeFromTypeAliasReference(node, symbol); + } + + if (symbol.flags & SymbolFlags.Value && node.kind === SyntaxKind.JSDocTypeReference) { + // A JSDocTypeReference may have resolved to a value (as opposed to a type). In + // that case, the type of this reference is just the type of the value we resolved + // to. + return getTypeOfSymbol(symbol); + } + + return getTypeFromNonGenericTypeReference(node, symbol); + } + + function getTypeFromTypeReference(node: TypeReferenceNode | ExpressionWithTypeArguments | JSDocTypeReference): Type { const links = getNodeLinks(node); if (!links.resolvedType) { - // We only support expressions that are simple qualified names. For other expressions this produces undefined. - const typeNameOrExpression = node.kind === SyntaxKind.TypeReference ? (node).typeName : - isSupportedExpressionWithTypeArguments(node) ? (node).expression : - undefined; - const symbol = typeNameOrExpression && resolveEntityName(typeNameOrExpression, SymbolFlags.Type) || unknownSymbol; - const type = symbol === unknownSymbol ? unknownType : - symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? getTypeFromClassOrInterfaceReference(node, symbol) : - symbol.flags & SymbolFlags.TypeAlias ? getTypeFromTypeAliasReference(node, symbol) : - getTypeFromNonGenericTypeReference(node, symbol); + let symbol: Symbol; + let type: Type; + if (node.kind === SyntaxKind.JSDocTypeReference) { + const typeReferenceName = getTypeReferenceName(node); + symbol = resolveTypeReferenceName(node, typeReferenceName); + type = getTypeReferenceType(node, symbol); + + links.resolvedSymbol = symbol; + links.resolvedType = type; + } + else { + // We only support expressions that are simple qualified names. For other expressions this produces undefined. + const typeNameOrExpression = node.kind === SyntaxKind.TypeReference ? (node).typeName : + isSupportedExpressionWithTypeArguments(node) ? (node).expression : + undefined; + symbol = typeNameOrExpression && resolveEntityName(typeNameOrExpression, SymbolFlags.Type) || unknownSymbol; + type = symbol === unknownSymbol ? unknownType : + symbol.flags & (SymbolFlags.Class | SymbolFlags.Interface) ? getTypeFromClassOrInterfaceReference(node, symbol) : + symbol.flags & SymbolFlags.TypeAlias ? getTypeFromTypeAliasReference(node, symbol) : + getTypeFromNonGenericTypeReference(node, symbol); + } // Cache both the resolved symbol and the resolved type. The resolved symbol is needed in when we check the // type reference in checkTypeReferenceOrExpressionWithTypeArguments. links.resolvedSymbol = symbol; @@ -4627,6 +4788,24 @@ namespace ts { return links.resolvedType; } + function getTypeFromJSDocVariadicType(node: JSDocVariadicType): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const type = getTypeFromTypeNode(node.type); + links.resolvedType = type ? createArrayType(type) : unknownType; + } + return links.resolvedType; + } + + function getTypeFromJSDocTupleType(node: JSDocTupleType): Type { + const links = getNodeLinks(node); + if (!links.resolvedType) { + const types = map(node.types, getTypeFromTypeNode); + links.resolvedType = createTupleType(types); + } + return links.resolvedType; + } + function getThisType(node: TypeNode): Type { const container = getThisContainer(node, /*includeArrowFunctions*/ false); const parent = container && container.parent; @@ -4670,6 +4849,8 @@ namespace ts { function getTypeFromTypeNode(node: TypeNode): Type { switch (node.kind) { case SyntaxKind.AnyKeyword: + case SyntaxKind.JSDocAllType: + case SyntaxKind.JSDocUnknownType: return anyType; case SyntaxKind.StringKeyword: return stringType; @@ -4686,6 +4867,7 @@ namespace ts { case SyntaxKind.StringLiteralType: return getTypeFromStringLiteralTypeNode(node); case SyntaxKind.TypeReference: + case SyntaxKind.JSDocTypeReference: return getTypeFromTypeReference(node); case SyntaxKind.TypePredicate: return getTypeFromPredicateTypeNode(node); @@ -4694,18 +4876,27 @@ namespace ts { case SyntaxKind.TypeQuery: return getTypeFromTypeQueryNode(node); case SyntaxKind.ArrayType: + case SyntaxKind.JSDocArrayType: return getTypeFromArrayTypeNode(node); case SyntaxKind.TupleType: return getTypeFromTupleTypeNode(node); case SyntaxKind.UnionType: + case SyntaxKind.JSDocUnionType: return getTypeFromUnionTypeNode(node); case SyntaxKind.IntersectionType: return getTypeFromIntersectionTypeNode(node); case SyntaxKind.ParenthesizedType: - return getTypeFromTypeNode((node).type); + case SyntaxKind.JSDocNullableType: + case SyntaxKind.JSDocNonNullableType: + case SyntaxKind.JSDocConstructorType: + case SyntaxKind.JSDocThisType: + case SyntaxKind.JSDocOptionalType: + return getTypeFromTypeNode((node).type); case SyntaxKind.FunctionType: case SyntaxKind.ConstructorType: case SyntaxKind.TypeLiteral: + case SyntaxKind.JSDocFunctionType: + case SyntaxKind.JSDocRecordType: return getTypeFromTypeLiteralOrFunctionOrConstructorTypeNode(node); // This function assumes that an identifier or qualified name is a type expression // Callers should first ensure this by calling isTypeNode @@ -4713,6 +4904,10 @@ namespace ts { case SyntaxKind.QualifiedName: const symbol = getSymbolAtLocation(node); return symbol && getDeclaredTypeOfSymbol(symbol); + case SyntaxKind.JSDocTupleType: + return getTypeFromJSDocTupleType(node); + case SyntaxKind.JSDocVariadicType: + return getTypeFromJSDocVariadicType(node); default: return unknownType; } @@ -7088,18 +7283,25 @@ namespace ts { return container.flags & NodeFlags.Static ? getTypeOfSymbol(symbol) : (getDeclaredTypeOfSymbol(symbol)).thisType; } - // If this is a function in a JS file, it might be a class method. Check if it's the RHS - // of a x.prototype.y = function [name]() { .... } - if (isInJavaScriptFile(node) && container.kind === SyntaxKind.FunctionExpression) { - if (getSpecialPropertyAssignmentKind(container.parent) === SpecialPropertyAssignmentKind.PrototypeProperty) { - // Get the 'x' of 'x.prototype.y = f' (here, 'f' is 'container') - const className = (((container.parent as BinaryExpression) // x.protoype.y = f - .left as PropertyAccessExpression) // x.prototype.y - .expression as PropertyAccessExpression) // x.prototype - .expression; // x - const classSymbol = checkExpression(className).symbol; - if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { - return getInferredClassType(classSymbol); + if (isInJavaScriptFile(node)) { + const type = getTypeForThisExpressionFromJSDoc(container); + if (type && type !== unknownType) { + return type; + } + + // If this is a function in a JS file, it might be a class method. Check if it's the RHS + // of a x.prototype.y = function [name]() { .... } + if (container.kind === SyntaxKind.FunctionExpression) { + if (getSpecialPropertyAssignmentKind(container.parent) === SpecialPropertyAssignmentKind.PrototypeProperty) { + // Get the 'x' of 'x.prototype.y = f' (here, 'f' is 'container') + const className = (((container.parent as BinaryExpression) // x.protoype.y = f + .left as PropertyAccessExpression) // x.prototype.y + .expression as PropertyAccessExpression) // x.prototype + .expression; // x + const classSymbol = checkExpression(className).symbol; + if (classSymbol && classSymbol.members && (classSymbol.flags & SymbolFlags.Function)) { + return getInferredClassType(classSymbol); + } } } } @@ -7107,6 +7309,16 @@ namespace ts { return anyType; } + function getTypeForThisExpressionFromJSDoc(node: Node) { + const typeTag = getJSDocTypeTag(node); + if (typeTag && typeTag.typeExpression.type.kind === SyntaxKind.JSDocFunctionType) { + const jsDocFunctionType = typeTag.typeExpression.type; + if (jsDocFunctionType.parameters.length > 0 && jsDocFunctionType.parameters[0].type.kind === SyntaxKind.JSDocThisType) { + return getTypeFromTypeNode(jsDocFunctionType.parameters[0].type); + } + } + } + function isInConstructorArgumentInitializer(node: Node, constructorDecl: Node): boolean { for (let n = node; n && n !== constructorDecl; n = n.parent) { if (n.kind === SyntaxKind.Parameter) { @@ -9914,7 +10126,8 @@ namespace ts { if (declaration && declaration.kind !== SyntaxKind.Constructor && declaration.kind !== SyntaxKind.ConstructSignature && - declaration.kind !== SyntaxKind.ConstructorType) { + declaration.kind !== SyntaxKind.ConstructorType && + !isJSDocConstructSignature(declaration)) { // When resolved signature is a call signature (and not a construct signature) the result type is any, unless // the declaring function had members created through 'x.prototype.y = expr' or 'this.y = expr' psuedodeclarations @@ -10034,6 +10247,13 @@ namespace ts { } } + function getReturnTypeFromJSDocComment(func: SignatureDeclaration | FunctionDeclaration): Type { + const returnTag = getJSDocReturnTag(func); + if (returnTag) { + return getTypeFromTypeNode(returnTag.typeExpression.type); + } + } + function createPromiseType(promisedType: Type): Type { // creates a `Promise` type where `T` is the promisedType argument const globalPromiseType = getGlobalPromiseType(); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index e391408b419ec..ecf64392a9a73 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -611,44 +611,24 @@ namespace ts { fixupParentReferences(sourceFile); } - // If this is a javascript file, proactively see if we can get JSDoc comments for - // relevant nodes in the file. We'll use these to provide typing informaion if they're - // available. - if (isSourceFileJavaScript(sourceFile)) { - addJSDocComments(); - } - return sourceFile; } - function addJSDocComments() { - forEachChild(sourceFile, visit); - return; - - function visit(node: Node) { - // Add additional cases as necessary depending on how we see JSDoc comments used - // in the wild. - switch (node.kind) { - case SyntaxKind.VariableStatement: - case SyntaxKind.FunctionDeclaration: - case SyntaxKind.Parameter: - addJSDocComment(node); - } - - forEachChild(node, visit); - } - } - function addJSDocComment(node: Node) { - const comments = getLeadingCommentRangesOfNode(node, sourceFile); - if (comments) { - for (const comment of comments) { - const jsDocComment = JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos); - if (jsDocComment) { - node.jsDocComment = jsDocComment; + function addJSDocComment(node: T): T { + if (contextFlags & ParserContextFlags.JavaScriptFile) { + const comments = getLeadingCommentRangesOfNode(node, sourceFile); + if (comments) { + for (const comment of comments) { + const jsDocComment = JSDocParser.parseJSDocComment(node, comment.pos, comment.end - comment.pos); + if (jsDocComment) { + node.jsDocComment = jsDocComment; + } } } } + + return node; } export function fixupParentReferences(sourceFile: Node) { @@ -2067,7 +2047,8 @@ namespace ts { // contexts. In addition, parameter initializers are semantically disallowed in // overload signatures. So parameter initializers are transitively disallowed in // ambient contexts. - return finishNode(node); + + return addJSDocComment(finishNode(node)); } function parseBindingElementInitializer(inParameter: boolean) { @@ -4773,7 +4754,7 @@ namespace ts { setModifiers(node, modifiers); node.declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false); parseSemicolon(); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseFunctionDeclaration(fullStart: number, decorators: NodeArray, modifiers: ModifiersArray): FunctionDeclaration { @@ -4787,7 +4768,7 @@ namespace ts { const isAsync = !!(node.flags & NodeFlags.Async); fillSignature(SyntaxKind.ColonToken, /*yieldContext*/ isGenerator, /*awaitContext*/ isAsync, /*requireCompleteParameterList*/ false, node); node.body = parseFunctionBlockOrSemicolon(isGenerator, isAsync, Diagnostics.or_expected); - return finishNode(node); + return addJSDocComment(finishNode(node)); } function parseConstructorDeclaration(pos: number, decorators: NodeArray, modifiers: ModifiersArray): ConstructorDeclaration { @@ -5624,23 +5605,19 @@ namespace ts { export function parseJSDocTypeExpressionForTests(content: string, start: number, length: number) { initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); - const jsDocTypeExpression = parseJSDocTypeExpression(start, length); + scanner.setText(content, start, length); + token = scanner.scan(); + const jsDocTypeExpression = parseJSDocTypeExpression(); const diagnostics = parseDiagnostics; clearState(); return jsDocTypeExpression ? { jsDocTypeExpression, diagnostics } : undefined; } - // Parses out a JSDoc type expression. The starting position should be right at the open - // curly in the type expression. Returns 'undefined' if it encounters any errors while parsing. + // Parses out a JSDoc type expression. /* @internal */ - export function parseJSDocTypeExpression(start: number, length: number): JSDocTypeExpression { - scanner.setText(sourceText, start, length); - - // Prime the first token for us to start processing. - token = nextToken(); - - const result = createNode(SyntaxKind.JSDocTypeExpression); + export function parseJSDocTypeExpression(): JSDocTypeExpression { + const result = createNode(SyntaxKind.JSDocTypeExpression, scanner.getTokenPos()); parseExpected(SyntaxKind.OpenBraceToken); result.type = parseJSDocTopLevelType(); @@ -5938,7 +5915,8 @@ namespace ts { export function parseIsolatedJSDocComment(content: string, start: number, length: number) { initializeState("file.js", content, ScriptTarget.Latest, /*isJavaScriptFile*/ true, /*_syntaxCursor:*/ undefined); - const jsDocComment = parseJSDocComment(/*parent:*/ undefined, start, length); + sourceFile = { languageVariant: LanguageVariant.Standard, text: content }; + const jsDocComment = parseJSDocCommentWorker(start, length); const diagnostics = parseDiagnostics; clearState(); @@ -5946,12 +5924,19 @@ namespace ts { } export function parseJSDocComment(parent: Node, start: number, length: number): JSDocComment { + const saveToken = token; + const saveParseDiagnosticsLength = parseDiagnostics.length; + const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; + const comment = parseJSDocCommentWorker(start, length); if (comment) { - fixupParentReferences(comment); comment.parent = parent; } + token = saveToken; + parseDiagnostics.length = saveParseDiagnosticsLength; + parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; + return comment; } @@ -5966,69 +5951,69 @@ namespace ts { Debug.assert(end <= content.length); let tags: NodeArray; - let pos: number; - - // NOTE(cyrusn): This is essentially a handwritten scanner for JSDocComments. I - // considered using an actual Scanner, but this would complicate things. The - // scanner would need to know it was in a Doc Comment. Otherwise, it would then - // produce comments *inside* the doc comment. In the end it was just easier to - // write a simple scanner rather than go that route. - if (length >= "/** */".length) { - if (content.charCodeAt(start) === CharacterCodes.slash && - content.charCodeAt(start + 1) === CharacterCodes.asterisk && - content.charCodeAt(start + 2) === CharacterCodes.asterisk && - content.charCodeAt(start + 3) !== CharacterCodes.asterisk) { + let result: JSDocComment; + + // Check for /** (JSDoc opening part) + if (content.charCodeAt(start) === CharacterCodes.slash && + content.charCodeAt(start + 1) === CharacterCodes.asterisk && + content.charCodeAt(start + 2) === CharacterCodes.asterisk && + content.charCodeAt(start + 3) !== CharacterCodes.asterisk) { + + + // + 3 for leading /**, - 5 in total for /** */ + scanner.scanRange(start + 3, length - 5, () => { // Initially we can parse out a tag. We also have seen a starting asterisk. // This is so that /** * @type */ doesn't parse. let canParseTag = true; let seenAsterisk = true; - for (pos = start + "/**".length; pos < end; ) { - const ch = content.charCodeAt(pos); - pos++; - - if (ch === CharacterCodes.at && canParseTag) { - parseTag(); - - // Once we parse out a tag, we cannot keep parsing out tags on this line. - canParseTag = false; - continue; - } + nextJSDocToken(); + while (token !== SyntaxKind.EndOfFileToken) { + switch (token) { + case SyntaxKind.AtToken: + if (canParseTag) { + parseTag(); + } + // This will take us to the end of the line, so it's OK to parse a tag on the next pass through the loop + seenAsterisk = false; + break; + + case SyntaxKind.NewLineTrivia: + // After a line break, we can parse a tag, and we haven't seen an asterisk on the next line yet + canParseTag = true; + seenAsterisk = false; + break; + + case SyntaxKind.AsteriskToken: + if (seenAsterisk) { + // If we've already seen an asterisk, then we can no longer parse a tag on this line + canParseTag = false; + } + // Ignore the first asterisk on a line + seenAsterisk = true; + break; + + case SyntaxKind.Identifier: + // Anything else is doc comment text. We can't do anything with it. Because it + // wasn't a tag, we can no longer parse a tag on this line until we hit the next + // line break. + canParseTag = false; + break; - if (isLineBreak(ch)) { - // After a line break, we can parse a tag, and we haven't seen as asterisk - // on the next line yet. - canParseTag = true; - seenAsterisk = false; - continue; + case SyntaxKind.EndOfFileToken: + break; } - if (isWhiteSpace(ch)) { - // Whitespace doesn't affect any of our parsing. - continue; - } + nextJSDocToken(); + } - // Ignore the first asterisk on a line. - if (ch === CharacterCodes.asterisk) { - if (seenAsterisk) { - // If we've already seen an asterisk, then we can no longer parse a tag - // on this line. - canParseTag = false; - } - seenAsterisk = true; - continue; - } + result = createJSDocComment(); - // Anything else is doc comment text. We can't do anything with it. Because it - // wasn't a tag, we can no longer parse a tag on this line until we hit the next - // line break. - canParseTag = false; - } - } + }); } - return createJSDocComment(); + return result; function createJSDocComment(): JSDocComment { if (!tags) { @@ -6041,17 +6026,18 @@ namespace ts { } function skipWhitespace(): void { - while (pos < end && isWhiteSpace(content.charCodeAt(pos))) { - pos++; + while (token === SyntaxKind.WhitespaceTrivia || token === SyntaxKind.NewLineTrivia) { + nextJSDocToken(); } } function parseTag(): void { - Debug.assert(content.charCodeAt(pos - 1) === CharacterCodes.at); - const atToken = createNode(SyntaxKind.AtToken, pos - 1); - atToken.end = pos; + Debug.assert(token === SyntaxKind.AtToken); + const atToken = createNode(SyntaxKind.AtToken, scanner.getTokenPos()); + atToken.end = scanner.getTextPos(); + nextJSDocToken(); - const tagName = scanIdentifier(); + const tagName = parseJSDocIdentifier(); if (!tagName) { return; } @@ -6082,7 +6068,7 @@ namespace ts { const result = createNode(SyntaxKind.JSDocTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; - return finishNode(result, pos); + return finishNode(result); } function addTag(tag: JSDocTag): void { @@ -6098,14 +6084,11 @@ namespace ts { } function tryParseTypeExpression(): JSDocTypeExpression { - skipWhitespace(); - - if (content.charCodeAt(pos) !== CharacterCodes.openBrace) { + if (token !== SyntaxKind.OpenBraceToken) { return undefined; } - const typeExpression = parseJSDocTypeExpression(pos, end - pos); - pos = typeExpression.end; + const typeExpression = parseJSDocTypeExpression(); return typeExpression; } @@ -6115,18 +6098,25 @@ namespace ts { skipWhitespace(); let name: Identifier; let isBracketed: boolean; - if (content.charCodeAt(pos) === CharacterCodes.openBracket) { - pos++; - skipWhitespace(); - name = scanIdentifier(); + // Looking for something like '[foo]' or 'foo' + if (parseOptionalToken(SyntaxKind.OpenBracketToken)) { + name = parseJSDocIdentifier(); isBracketed = true; + + // May have an optional default, e.g. '[foo = 42]' + if (parseOptionalToken(SyntaxKind.EqualsToken)) { + parseExpression(); + } + + parseExpected(SyntaxKind.CloseBracketToken); } - else { - name = scanIdentifier(); + else if (token === SyntaxKind.Identifier) { + name = parseJSDocIdentifier(); } if (!name) { - parseErrorAtPosition(pos, 0, Diagnostics.Identifier_expected); + parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected); + return undefined; } let preName: Identifier, postName: Identifier; @@ -6148,95 +6138,90 @@ namespace ts { result.typeExpression = typeExpression; result.postParameterName = postName; result.isBracketed = isBracketed; - return finishNode(result, pos); + return finishNode(result); } function handleReturnTag(atToken: Node, tagName: Identifier): JSDocReturnTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocReturnTag)) { - parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } const result = createNode(SyntaxKind.JSDocReturnTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; result.typeExpression = tryParseTypeExpression(); - return finishNode(result, pos); + return finishNode(result); } function handleTypeTag(atToken: Node, tagName: Identifier): JSDocTypeTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocTypeTag)) { - parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } const result = createNode(SyntaxKind.JSDocTypeTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; result.typeExpression = tryParseTypeExpression(); - return finishNode(result, pos); + return finishNode(result); } function handleTemplateTag(atToken: Node, tagName: Identifier): JSDocTemplateTag { if (forEach(tags, t => t.kind === SyntaxKind.JSDocTemplateTag)) { - parseErrorAtPosition(tagName.pos, pos - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); + parseErrorAtPosition(tagName.pos, scanner.getTokenPos() - tagName.pos, Diagnostics._0_tag_already_specified, tagName.text); } + // Type parameter list looks like '@template T,U,V' const typeParameters = >[]; - typeParameters.pos = pos; + typeParameters.pos = scanner.getStartPos(); while (true) { - skipWhitespace(); - - const startPos = pos; - const name = scanIdentifier(); + const name = parseJSDocIdentifier(); if (!name) { - parseErrorAtPosition(startPos, 0, Diagnostics.Identifier_expected); + parseErrorAtPosition(scanner.getStartPos(), 0, Diagnostics.Identifier_expected); return undefined; } const typeParameter = createNode(SyntaxKind.TypeParameter, name.pos); typeParameter.name = name; - finishNode(typeParameter, pos); + finishNode(typeParameter); typeParameters.push(typeParameter); - skipWhitespace(); - if (content.charCodeAt(pos) !== CharacterCodes.comma) { + if (token === SyntaxKind.CommaToken) { + nextJSDocToken(); + } + else { break; } - - pos++; } - typeParameters.end = pos; - const result = createNode(SyntaxKind.JSDocTemplateTag, atToken.pos); result.atToken = atToken; result.tagName = tagName; result.typeParameters = typeParameters; - return finishNode(result, pos); + finishNode(result); + typeParameters.end = result.end; + return result; } - function scanIdentifier(): Identifier { - const startPos = pos; - for (; pos < end; pos++) { - const ch = content.charCodeAt(pos); - if (pos === startPos && isIdentifierStart(ch, ScriptTarget.Latest)) { - continue; - } - else if (pos > startPos && isIdentifierPart(ch, ScriptTarget.Latest)) { - continue; - } - - break; - } + function nextJSDocToken(): SyntaxKind { + return token = scanner.scanJSDocToken(); + } - if (startPos === pos) { + function parseJSDocIdentifier(): Identifier { + if (token !== SyntaxKind.Identifier) { + parseErrorAtCurrentToken(Diagnostics.Identifier_expected); return undefined; } - const result = createNode(SyntaxKind.Identifier, startPos); - result.text = content.substring(startPos, pos); - return finishNode(result, pos); + const pos = scanner.getTokenPos(); + const end = scanner.getTextPos(); + const result = createNode(SyntaxKind.Identifier, pos); + result.text = content.substring(pos, end); + finishNode(result, end); + + nextJSDocToken(); + return result; } } } diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index 7192ba6b40cce..e716da202505f 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -29,6 +29,7 @@ namespace ts { scanJsxIdentifier(): SyntaxKind; reScanJsxToken(): SyntaxKind; scanJsxToken(): SyntaxKind; + scanJSDocToken(): SyntaxKind; scan(): SyntaxKind; // Sets the text for the scanner to scan. An optional subrange starting point and length // can be provided to have the scanner only scan a portion of the text. @@ -42,6 +43,10 @@ namespace ts { // is returned from this function. lookAhead(callback: () => T): T; + // Invokes the callback with the scanner set to scan the specified range. When the callback + // returns, the scanner is restored to the state it was in before scanRange was called. + scanRange(start: number, length: number, callback: () => T): T; + // Invokes the provided callback. If the callback returns something falsy, then it restores // the scanner to the state it was in immediately prior to invoking the callback. If the // callback returns something truthy, then the scanner state is not rolled back. The result @@ -750,6 +755,7 @@ namespace ts { scanJsxIdentifier, reScanJsxToken, scanJsxToken, + scanJSDocToken, scan, setText, setScriptTarget, @@ -758,6 +764,7 @@ namespace ts { setTextPos, tryScan, lookAhead, + scanRange, }; function error(message: DiagnosticMessage, length?: number): void { @@ -1665,6 +1672,60 @@ namespace ts { return token; } + function scanJSDocToken(): SyntaxKind { + if (pos >= end) { + return token = SyntaxKind.EndOfFileToken; + } + + startPos = pos; + + // Eat leading whitespace + let ch = text.charCodeAt(pos); + while (pos < end) { + ch = text.charCodeAt(pos); + if (isWhiteSpace(ch)) { + pos++; + } + else { + break; + } + } + tokenPos = pos; + + switch (ch) { + case CharacterCodes.at: + return pos += 1, token = SyntaxKind.AtToken; + case CharacterCodes.lineFeed: + case CharacterCodes.carriageReturn: + return pos += 1, token = SyntaxKind.NewLineTrivia; + case CharacterCodes.asterisk: + return pos += 1, token = SyntaxKind.AsteriskToken; + case CharacterCodes.openBrace: + return pos += 1, token = SyntaxKind.OpenBraceToken; + case CharacterCodes.closeBrace: + return pos += 1, token = SyntaxKind.CloseBraceToken; + case CharacterCodes.openBracket: + return pos += 1, token = SyntaxKind.OpenBracketToken; + case CharacterCodes.closeBracket: + return pos += 1, token = SyntaxKind.CloseBracketToken; + case CharacterCodes.equals: + return pos += 1, token = SyntaxKind.EqualsToken; + case CharacterCodes.comma: + return pos += 1, token = SyntaxKind.CommaToken; + } + + if (isIdentifierStart(ch, ScriptTarget.Latest)) { + pos++; + while (isIdentifierPart(text.charCodeAt(pos), ScriptTarget.Latest) && pos < end) { + pos++; + } + return token = SyntaxKind.Identifier; + } + else { + return pos += 1, token = SyntaxKind.Unknown; + } + } + function speculationHelper(callback: () => T, isLookahead: boolean): T { const savePos = pos; const saveStartPos = startPos; @@ -1687,6 +1748,33 @@ namespace ts { return result; } + function scanRange(start: number, length: number, callback: () => T): T { + const saveEnd = end; + const savePos = pos; + const saveStartPos = startPos; + const saveTokenPos = tokenPos; + const saveToken = token; + const savePrecedingLineBreak = precedingLineBreak; + const saveTokenValue = tokenValue; + const saveHasExtendedUnicodeEscape = hasExtendedUnicodeEscape; + const saveTokenIsUnterminated = tokenIsUnterminated; + + setText(text, start, length); + const result = callback(); + + end = saveEnd; + pos = savePos; + startPos = saveStartPos; + tokenPos = saveTokenPos; + token = saveToken; + precedingLineBreak = savePrecedingLineBreak; + tokenValue = saveTokenValue; + hasExtendedUnicodeEscape = saveHasExtendedUnicodeEscape; + tokenIsUnterminated = saveTokenIsUnterminated; + + return result; + } + function lookAhead(callback: () => T): T { return speculationHelper(callback, /*isLookahead*/ true); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 18dc6602abe70..ca94060d86900 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -312,11 +312,11 @@ namespace ts { // Top-level nodes SourceFile, - // JSDoc nodes. + // JSDoc nodes JSDocTypeExpression, - // The * type. + // The * type JSDocAllType, - // The ? type. + // The ? type JSDocUnknownType, JSDocArrayType, JSDocUnionType, @@ -1004,7 +1004,7 @@ namespace ts { } // @kind(SyntaxKind.CallExpression) - export interface CallExpression extends LeftHandSideExpression { + export interface CallExpression extends LeftHandSideExpression, Declaration { expression: LeftHandSideExpression; typeArguments?: NodeArray; arguments: NodeArray; @@ -1483,6 +1483,8 @@ namespace ts { type: JSDocType; } + export type JSDocTypeReferencingNode = JSDocThisType | JSDocConstructorType | JSDocVariadicType | JSDocOptionalType | JSDocNullableType | JSDocNonNullableType; + // @kind(SyntaxKind.JSDocRecordMember) export interface JSDocRecordMember extends PropertySignature { name: Identifier | LiteralExpression; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 4636c8d144cf4..4622f92b5df21 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1054,7 +1054,7 @@ namespace ts { /** * Returns true if the node is a CallExpression to the identifier 'require' with - * exactly one string literal argument. + * exactly one argument. * This function does not test if the node is in a JavaScript file or not. */ export function isRequireCall(expression: Node): expression is CallExpression { @@ -1062,8 +1062,7 @@ namespace ts { return expression.kind === SyntaxKind.CallExpression && (expression).expression.kind === SyntaxKind.Identifier && ((expression).expression).text === "require" && - (expression).arguments.length === 1 && - (expression).arguments[0].kind === SyntaxKind.StringLiteral; + (expression).arguments.length === 1; } /// Given a BinaryExpression, returns SpecialPropertyAssignmentKind for the various kinds of property @@ -1143,26 +1142,56 @@ namespace ts { (node).parameters[0].type.kind === SyntaxKind.JSDocConstructorType; } - function getJSDocTag(node: Node, kind: SyntaxKind): JSDocTag { - if (node && node.jsDocComment) { - for (const tag of node.jsDocComment.tags) { - if (tag.kind === kind) { - return tag; - } + function getJSDocTag(node: Node, kind: SyntaxKind, checkParentVariableStatement: boolean): JSDocTag { + if (!node) { + return undefined; + } + + const jsDocComment = getJSDocComment(node, checkParentVariableStatement); + if (!jsDocComment) { + return undefined; + } + + for (const tag of jsDocComment.tags) { + if (tag.kind === kind) { + return tag; } } } + function getJSDocComment(node: Node, checkParentVariableStatement: boolean): JSDocComment { + if (node.jsDocComment) { + return node.jsDocComment; + } + // Try to recognize this pattern when node is initializer of variable declaration and JSDoc comments are on containing variable statement. + // /** + // * @param {number} name + // * @returns {number} + // */ + // var x = function(name) { return name.length; } + if (checkParentVariableStatement) { + const isInitializerOfVariableDeclarationInStatement = + node.parent.kind === SyntaxKind.VariableDeclaration && + (node.parent).initializer === node && + node.parent.parent.parent.kind === SyntaxKind.VariableStatement; + + const variableStatementNode = isInitializerOfVariableDeclarationInStatement ? node.parent.parent.parent : undefined; + return variableStatementNode && variableStatementNode.jsDocComment; + } + + return undefined; + } + export function getJSDocTypeTag(node: Node): JSDocTypeTag { - return getJSDocTag(node, SyntaxKind.JSDocTypeTag); + return getJSDocTag(node, SyntaxKind.JSDocTypeTag, /*checkParentVariableStatement*/ false); } export function getJSDocReturnTag(node: Node): JSDocReturnTag { - return getJSDocTag(node, SyntaxKind.JSDocReturnTag); + return getJSDocTag(node, SyntaxKind.JSDocReturnTag, /*checkParentVariableStatement*/ true); } export function getJSDocTemplateTag(node: Node): JSDocTemplateTag { - return getJSDocTag(node, SyntaxKind.JSDocTemplateTag); + return getJSDocTag(node, SyntaxKind.JSDocTemplateTag, /*checkParentVariableStatement*/ false); } export function getCorrespondingJSDocParameterTag(parameter: ParameterDeclaration): JSDocParameterTag { @@ -1171,19 +1200,21 @@ namespace ts { // annotation. const parameterName = (parameter.name).text; - const docComment = parameter.parent.jsDocComment; - if (docComment) { - return forEach(docComment.tags, t => { - if (t.kind === SyntaxKind.JSDocParameterTag) { - const parameterTag = t; + const jsDocComment = getJSDocComment(parameter.parent, /*checkParentVariableStatement*/ true); + if (jsDocComment) { + for (const tag of jsDocComment.tags) { + if (tag.kind === SyntaxKind.JSDocParameterTag) { + const parameterTag = tag; const name = parameterTag.preParameterName || parameterTag.postParameterName; if (name.text === parameterName) { - return t; + return parameterTag; } } - }); + } } } + + return undefined; } export function hasRestParameter(s: SignatureDeclaration): boolean { diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 29a3383631d10..ee1bf4846dff7 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -572,6 +572,31 @@ namespace FourSlash { } } + public verifyCompletionListStartsWithItemsInOrder(items: string[]): void { + if (items.length === 0) { + return; + } + + const entries = this.getCompletionListAtCaret().entries; + assert.isTrue(items.length <= entries.length, `Amount of expected items in completion list [ ${items.length} ] is greater than actual number of items in list [ ${entries.length} ]`); + for (let i = 0; i < items.length; i++) { + assert.equal(entries[i].name, items[i], `Unexpected item in completion list`); + } + } + + public noItemsWithSameNameButDifferentKind(): void { + const completions = this.getCompletionListAtCaret(); + const uniqueItems: ts.Map = {}; + for (const item of completions.entries) { + if (!ts.hasProperty(uniqueItems, item.name)) { + uniqueItems[item.name] = item.kind; + } + else { + assert.equal(item.kind, uniqueItems[item.name], `Items should have the same kind, got ${item.kind} and ${uniqueItems[item.name]}`); + } + } + } + public verifyMemberListIsEmpty(negative: boolean) { const members = this.getMemberListAtCaret(); if ((!members || members.entries.length === 0) && negative) { diff --git a/src/server/session.ts b/src/server/session.ts index 9b9bf35a4a588..c1bafdf5ba80b 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -792,7 +792,9 @@ namespace ts.server { } private closeClientFile(fileName: string) { - if (!fileName) { return; } + if (!fileName) { + return; + } const file = ts.normalizePath(fileName); this.projectService.closeClientFile(file); } diff --git a/tests/baselines/reference/jsFileCompilationSyntaxError.errors.txt b/tests/baselines/reference/jsFileCompilationSyntaxError.errors.txt index 6e55ff30f04fd..d98a1e7e7bbc8 100644 --- a/tests/baselines/reference/jsFileCompilationSyntaxError.errors.txt +++ b/tests/baselines/reference/jsFileCompilationSyntaxError.errors.txt @@ -1,14 +1,11 @@ error TS5055: Cannot write file 'tests/cases/compiler/a.js' because it would overwrite input file. -tests/cases/compiler/a.js(3,6): error TS1223: 'type' tag already specified. !!! error TS5055: Cannot write file 'tests/cases/compiler/a.js' because it would overwrite input file. -==== tests/cases/compiler/a.js (1 errors) ==== +==== tests/cases/compiler/a.js (0 errors) ==== /** * @type {number} * @type {string} - ~~~~ -!!! error TS1223: 'type' tag already specified. */ var v; \ No newline at end of file diff --git a/tests/cases/fourslash/completionInJsDoc.ts b/tests/cases/fourslash/completionInJsDoc.ts index 8051630385649..e5cc7a3d2b609 100644 --- a/tests/cases/fourslash/completionInJsDoc.ts +++ b/tests/cases/fourslash/completionInJsDoc.ts @@ -23,11 +23,8 @@ ////// @pa/*7*/ ////var v7; //// -/////** @param { n/*8*/ } */ +/////** @return { n/*8*/ } */ ////var v8; -//// -/////** @return { n/*9*/ } */ -////var v9; goTo.marker('1'); verify.completionListContains("constructor"); @@ -57,6 +54,3 @@ verify.completionListIsEmpty(); goTo.marker('8'); verify.completionListContains('number'); -goTo.marker('9'); -verify.completionListContains('number'); - diff --git a/tests/cases/fourslash/getJavaScriptCompletions1.ts b/tests/cases/fourslash/getJavaScriptCompletions1.ts new file mode 100644 index 0000000000000..6d8f36aac1b51 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions1.ts @@ -0,0 +1,10 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {number} */ +////var v; +////v./**/ + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/getJavaScriptCompletions10.ts b/tests/cases/fourslash/getJavaScriptCompletions10.ts new file mode 100644 index 0000000000000..5fbd91e89881a --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions10.ts @@ -0,0 +1,11 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @type {function(this:number)} +//// */ +////function f() { this./**/ } + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions11.ts b/tests/cases/fourslash/getJavaScriptCompletions11.ts new file mode 100644 index 0000000000000..f12f53fa960fe --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions11.ts @@ -0,0 +1,11 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {number|string} */ +////var v; +////v./**/ + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); +verify.completionListContains("charCodeAt", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions12.ts b/tests/cases/fourslash/getJavaScriptCompletions12.ts new file mode 100644 index 0000000000000..99b1482885440 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions12.ts @@ -0,0 +1,36 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @param {number} input +//// * @param {string} currency +//// * @returns {number} +//// */ +////var convert = function(input, currency) { +//// switch(currency./*1*/) { +//// case "USD": +//// input./*2*/; +//// case "EUR": +//// return "" + rateToUsd.EUR; +//// case "CNY": +//// return {} + rateToUsd.CNY; +//// } +////} +////convert(1, "")./*3*/ +/////** +//// * @param {number} x +//// */ +////var test1 = function(x) { return x./*4*/ }, test2 = function(a) { return a./*5*/ }; + + +goTo.marker("1"); +verify.completionListContains("charCodeAt", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); +goTo.marker("2"); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); +goTo.marker("3"); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); +goTo.marker("4"); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); +goTo.marker("5"); +verify.completionListContains("test1", /*displayText:*/ undefined, /*documentation*/ undefined, "warning"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions13.ts b/tests/cases/fourslash/getJavaScriptCompletions13.ts new file mode 100644 index 0000000000000..438c73e304c04 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions13.ts @@ -0,0 +1,26 @@ +/// +// @allowNonTsExtensions: true + +// @Filename: file1.js + +////var file1Identifier = 1; +////interface Foo { FooProp: number }; + +// @Filename: file2.js + +////var file2Identifier1 = 2; +////var file2Identifier2 = 2; +/////*1*/ +////file2Identifier2./*2*/ + +goTo.marker("1"); +verify.completionListContains("file2Identifier1"); +verify.completionListContains("file2Identifier2"); +verify.completionListContains("file1Identifier"); +verify.not.completionListContains("FooProp"); + +goTo.marker("2"); +verify.completionListContains("file2Identifier1"); +verify.completionListContains("file2Identifier2"); +verify.not.completionListContains("file1Identifier") +verify.not.completionListContains("FooProp"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions14.ts b/tests/cases/fourslash/getJavaScriptCompletions14.ts new file mode 100644 index 0000000000000..52a23065a5e2e --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions14.ts @@ -0,0 +1,13 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: file1.js +/////// +////interface Number { +//// toExponential(fractionDigits?: number): string; +////} +////var x = 1; +////x./*1*/ + +goTo.marker("1"); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/getJavaScriptCompletions15.ts b/tests/cases/fourslash/getJavaScriptCompletions15.ts new file mode 100644 index 0000000000000..fbcbc2d4692a9 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions15.ts @@ -0,0 +1,29 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: refFile1.ts +//// export var V = 1; + +// @Filename: refFile2.ts +//// export var V = "123" + +// @Filename: refFile3.ts +//// export var V = "123" + +// @Filename: main.js +//// import ref1 = require("refFile1"); +//// var ref2 = require("refFile2"); +//// ref1.V./*1*/; +//// ref2.V./*2*/; +//// var v = { x: require("refFile3") }; +//// v.x./*3*/; +//// v.x.V./*4*/; + +goTo.marker("1"); +verify.completionListContains("toExponential"); +goTo.marker("2"); +verify.completionListContains("toLowerCase"); +goTo.marker("3"); +verify.completionListContains("V"); +goTo.marker("4"); +verify.completionListContains("toLowerCase"); diff --git a/tests/cases/fourslash/getJavaScriptCompletions2.ts b/tests/cases/fourslash/getJavaScriptCompletions2.ts new file mode 100644 index 0000000000000..0cb2de046a668 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions2.ts @@ -0,0 +1,10 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {(number|string)} */ +////var v; +////v./**/ + +goTo.marker(); +verify.completionListContains("valueOf", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions3.ts b/tests/cases/fourslash/getJavaScriptCompletions3.ts new file mode 100644 index 0000000000000..13a6f1673e6b3 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions3.ts @@ -0,0 +1,10 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {Array.} */ +////var v; +////v./**/ + +goTo.marker(); +verify.completionListContains("concat", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions4.ts b/tests/cases/fourslash/getJavaScriptCompletions4.ts new file mode 100644 index 0000000000000..3719b8e1258cb --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions4.ts @@ -0,0 +1,10 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @return {number} */ +////function foo(a,b) { } +////foo(1,2)./**/ + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions5.ts b/tests/cases/fourslash/getJavaScriptCompletions5.ts new file mode 100644 index 0000000000000..348feef94c329 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions5.ts @@ -0,0 +1,15 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// /** +//// * @template T +//// * @param {T} a +//// * @return {T} */ +//// function foo(a) { } +//// let x = /*1*/foo; +//// foo(1)./**/ + +goTo.marker('1'); +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); diff --git a/tests/cases/fourslash/getJavaScriptCompletions6.ts b/tests/cases/fourslash/getJavaScriptCompletions6.ts new file mode 100644 index 0000000000000..9f9a42b5762c2 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions6.ts @@ -0,0 +1,13 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @param {...number} a +//// */ +////function foo(a) { +//// a./**/ +////} + +goTo.marker(); +verify.completionListContains("concat", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions7.ts b/tests/cases/fourslash/getJavaScriptCompletions7.ts new file mode 100644 index 0000000000000..d8b01cfb757c2 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions7.ts @@ -0,0 +1,13 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @param {...number} a +//// */ +////function foo(a) { +//// a[0]./**/ +////} + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions8.ts b/tests/cases/fourslash/getJavaScriptCompletions8.ts new file mode 100644 index 0000000000000..418aae676bee5 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions8.ts @@ -0,0 +1,12 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @type {function(): number} +//// */ +////var v; +////v()./**/ + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptCompletions9.ts b/tests/cases/fourslash/getJavaScriptCompletions9.ts new file mode 100644 index 0000000000000..1619a2ca9fc49 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptCompletions9.ts @@ -0,0 +1,12 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** +//// * @type {function(new:number)} +//// */ +////var v; +////new v()./**/ + +goTo.marker(); +verify.completionListContains("toExponential", /*displayText:*/ undefined, /*documentation*/ undefined, "method"); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo1.ts b/tests/cases/fourslash/getJavaScriptQuickInfo1.ts new file mode 100644 index 0000000000000..71072f1699aa2 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo1.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {function(new:string,number)} */ +////var /**/v; + +goTo.marker(); +verify.quickInfoIs('var v: new (p1: number) => string'); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo2.ts b/tests/cases/fourslash/getJavaScriptQuickInfo2.ts new file mode 100644 index 0000000000000..ce347a62b2727 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo2.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @param {number} [a] */ +////function /**/f(a) { } + +goTo.marker(); +verify.quickInfoIs('function f(a?: number): void'); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo3.ts b/tests/cases/fourslash/getJavaScriptQuickInfo3.ts new file mode 100644 index 0000000000000..2e094bd21f6f2 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo3.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @param {number[]} [a] */ +////function /**/f(a) { } + +goTo.marker(); +verify.quickInfoIs('function f(a?: number[]): void'); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo4.ts b/tests/cases/fourslash/getJavaScriptQuickInfo4.ts new file mode 100644 index 0000000000000..34ee10d97b747 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo4.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @param {[number,string]} [a] */ +////function /**/f(a) { } + +goTo.marker(); +verify.quickInfoIs('function f(a?: [number, string]): void'); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo5.ts b/tests/cases/fourslash/getJavaScriptQuickInfo5.ts new file mode 100644 index 0000000000000..004af0811b7ac --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo5.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @param {{b:number}} [a] */ +////function /**/f(a) { } + +goTo.marker(); +verify.quickInfoIs('function f(a?: {\n b: number;\n}): void'); \ No newline at end of file diff --git a/tests/cases/fourslash/getJavaScriptQuickInfo6.ts b/tests/cases/fourslash/getJavaScriptQuickInfo6.ts new file mode 100644 index 0000000000000..3aebd19deda12 --- /dev/null +++ b/tests/cases/fourslash/getJavaScriptQuickInfo6.ts @@ -0,0 +1,9 @@ +/// + +// @allowNonTsExtensions: true +// @Filename: Foo.js +/////** @type {function(this:number)} */ +////function f() { /**/this } + +goTo.marker(); +verify.quickInfoIs('number'); \ No newline at end of file diff --git a/tests/cases/fourslash/javaScriptModulesError1.ts b/tests/cases/fourslash/javaScriptModulesError1.ts new file mode 100644 index 0000000000000..f13b25a182efb --- /dev/null +++ b/tests/cases/fourslash/javaScriptModulesError1.ts @@ -0,0 +1,12 @@ +/// + +// Error: Having more function parameters than entries in the dependency array + +// @allowNonTsExtensions: true +// @Filename: Foo.js +//// define('mod1', ['a'], /**/function(a, b) { +//// +//// }); + +// TODO: what should happen? +goTo.marker(); \ No newline at end of file diff --git a/tests/cases/unittests/jsDocParsing.ts b/tests/cases/unittests/jsDocParsing.ts index ebba6e8a1a648..9988383b467da 100644 --- a/tests/cases/unittests/jsDocParsing.ts +++ b/tests/cases/unittests/jsDocParsing.ts @@ -1,4 +1,5 @@ /// +/// /// /// @@ -985,15 +986,29 @@ module ts { describe("DocComments", () => { function parsesCorrectly(content: string, expected: string) { let comment = parseIsolatedJSDocComment(content); - Debug.assert(comment && comment.diagnostics.length === 0); + if (!comment) { + Debug.fail('Comment failed to parse entirely'); + } + if (comment.diagnostics.length > 0) { + Debug.fail('Comment has at least one diagnostic: ' + comment.diagnostics[0].messageText); + } let result = JSON.stringify(comment.jsDocComment, (k, v) => { return v && v.pos !== undefined ? JSON.parse(Utils.sourceFileToJSON(v)) : v; - }, " "); + }, 4); - assert.equal(result, expected); + if (result !== expected) { + // Turn on a human-readable diff + if (typeof require !== 'undefined') { + require('chai').config.showDiff = true; + chai.expect(JSON.parse(result)).equal(JSON.parse(expected)); + } + else { + assert.equal(result, expected); + } + } } function parsesIncorrectly(content: string) { @@ -1577,7 +1592,7 @@ module ts { "0": { "kind": "JSDocParameterTag", "pos": 8, - "end": 30, + "end": 31, "atToken": { "kind": "AtToken", "pos": 8, @@ -1609,7 +1624,7 @@ module ts { }, "length": 1, "pos": 8, - "end": 30 + "end": 31 } }`); }); @@ -1627,7 +1642,7 @@ module ts { "0": { "kind": "JSDocParameterTag", "pos": 8, - "end": 31, + "end": 36, "atToken": { "kind": "AtToken", "pos": 8, @@ -1659,7 +1674,7 @@ module ts { }, "length": 1, "pos": 8, - "end": 31 + "end": 36 } }`); }); @@ -2113,7 +2128,7 @@ module ts { "0": { "kind": "JSDocTemplateTag", "pos": 8, - "end": 24, + "end": 23, "atToken": { "kind": "AtToken", "pos": 8, @@ -2150,12 +2165,12 @@ module ts { }, "length": 2, "pos": 17, - "end": 24 + "end": 23 } }, "length": 1, "pos": 8, - "end": 24 + "end": 23 } }`); });