Skip to content

Support for JSDoc in services #15856

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
8 commits merged into from
May 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 40 additions & 28 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ namespace ts {
// during global merging in the checker. Why? The only case when ambient module is permitted inside another module is module augmentation
// and this case is specially handled. Module augmentations should only be merged with original module definition
// and should never be merged directly with other augmentation, and the latter case would be possible if automatic merge is allowed.
if (node.kind === SyntaxKind.JSDocTypedefTag) Debug.assert(isInJavaScriptFile(node)); // We shouldn't add symbols for JSDoc nodes if not in a JS file.
const isJSDocTypedefInJSDocNamespace = node.kind === SyntaxKind.JSDocTypedefTag &&
(node as JSDocTypedefTag).name &&
(node as JSDocTypedefTag).name.kind === SyntaxKind.Identifier &&
Expand Down Expand Up @@ -603,9 +604,7 @@ namespace ts {
// Binding of JsDocComment should be done before the current block scope container changes.
// because the scope of JsDocComment should not be affected by whether the current node is a
// container or not.
if (isInJavaScriptFile(node) && node.jsDoc) {
forEach(node.jsDoc, bind);
}
forEach(node.jsDoc, bind);
if (checkUnreachable(node)) {
bindEachChild(node);
return;
Expand Down Expand Up @@ -1913,9 +1912,7 @@ namespace ts {
// Here the current node is "foo", which is a container, but the scope of "MyType" should
// not be inside "foo". Therefore we always bind @typedef before bind the parent node,
// and skip binding this tag later when binding all the other jsdoc tags.
if (isInJavaScriptFile(node)) {
bindJSDocTypedefTagIfAny(node);
}
bindJSDocTypedefTagIfAny(node);

// First we bind declaration nodes to a symbol if possible. We'll both create a symbol
// and then potentially add the symbol to an appropriate symbol table. Possible
Expand Down Expand Up @@ -2003,7 +2000,7 @@ namespace ts {
// for typedef type names with namespaces, bind the new jsdoc type symbol here
// because it requires all containing namespaces to be in effect, namely the
// current "blockScopeContainer" needs to be set to its immediate namespace parent.
if ((<Identifier>node).isInJSDocNamespace) {
if (isInJavaScriptFile(node) && (<Identifier>node).isInJSDocNamespace) {
let parentNode = node.parent;
while (parentNode && parentNode.kind !== SyntaxKind.JSDocTypedefTag) {
parentNode = parentNode.parent;
Expand Down Expand Up @@ -2073,10 +2070,7 @@ namespace ts {
return bindVariableDeclarationOrBindingElement(<VariableDeclaration | BindingElement>node);
case SyntaxKind.PropertyDeclaration:
case SyntaxKind.PropertySignature:
case SyntaxKind.JSDocRecordMember:
return bindPropertyOrMethodOrAccessor(<Declaration>node, SymbolFlags.Property | ((<PropertyDeclaration>node).questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes);
case SyntaxKind.JSDocPropertyTag:
return bindJSDocProperty(<JSDocPropertyTag>node);
return bindPropertyWorker(node as PropertyDeclaration | PropertySignature);
case SyntaxKind.PropertyAssignment:
case SyntaxKind.ShorthandPropertyAssignment:
return bindPropertyOrMethodOrAccessor(<Declaration>node, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
Expand Down Expand Up @@ -2121,13 +2115,10 @@ namespace ts {
return bindPropertyOrMethodOrAccessor(<Declaration>node, SymbolFlags.SetAccessor, SymbolFlags.SetAccessorExcludes);
case SyntaxKind.FunctionType:
case SyntaxKind.ConstructorType:
case SyntaxKind.JSDocFunctionType:
return bindFunctionOrConstructorType(<SignatureDeclaration>node);
case SyntaxKind.TypeLiteral:
case SyntaxKind.MappedType:
case SyntaxKind.JSDocTypeLiteral:
case SyntaxKind.JSDocRecordType:
return bindAnonymousDeclaration(<Declaration>node, SymbolFlags.TypeLiteral, "__type");
return bindAnonymousTypeWorker(node as TypeLiteralNode | MappedTypeNode);
case SyntaxKind.ObjectLiteralExpression:
return bindObjectLiteralExpression(<ObjectLiteralExpression>node);
case SyntaxKind.FunctionExpression:
Expand All @@ -2148,11 +2139,6 @@ namespace ts {
return bindClassLikeDeclaration(<ClassLikeDeclaration>node);
case SyntaxKind.InterfaceDeclaration:
return bindBlockScopedDeclaration(<Declaration>node, SymbolFlags.Interface, SymbolFlags.InterfaceExcludes);
case SyntaxKind.JSDocTypedefTag:
if (!(<JSDocTypedefTag>node).fullName || (<JSDocTypedefTag>node).fullName.kind === SyntaxKind.Identifier) {
return bindBlockScopedDeclaration(<Declaration>node, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
}
break;
case SyntaxKind.TypeAliasDeclaration:
return bindBlockScopedDeclaration(<Declaration>node, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
case SyntaxKind.EnumDeclaration:
Expand Down Expand Up @@ -2190,9 +2176,41 @@ namespace ts {
// falls through
case SyntaxKind.ModuleBlock:
return updateStrictModeStatementList((<Block | ModuleBlock>node).statements);

default:
if (isInJavaScriptFile(node)) return bindJSDocWorker(node);
}
}

function bindJSDocWorker(node: Node) {
switch (node.kind) {
case SyntaxKind.JSDocRecordMember:
return bindPropertyWorker(node as JSDocRecordMember);
case SyntaxKind.JSDocPropertyTag:
return declareSymbolAndAddToSymbolTable(node as JSDocPropertyTag, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
case SyntaxKind.JSDocFunctionType:
return bindFunctionOrConstructorType(<SignatureDeclaration>node);
case SyntaxKind.JSDocTypeLiteral:
case SyntaxKind.JSDocRecordType:
return bindAnonymousTypeWorker(node as JSDocTypeLiteral | JSDocRecordType);
case SyntaxKind.JSDocTypedefTag: {
const { fullName } = node as JSDocTypedefTag;
if (!fullName || fullName.kind === SyntaxKind.Identifier) {
return bindBlockScopedDeclaration(<Declaration>node, SymbolFlags.TypeAlias, SymbolFlags.TypeAliasExcludes);
}
break;
}
}
}

function bindPropertyWorker(node: PropertyDeclaration | PropertySignature) {
return bindPropertyOrMethodOrAccessor(node, SymbolFlags.Property | (node.questionToken ? SymbolFlags.Optional : SymbolFlags.None), SymbolFlags.PropertyExcludes);
}

function bindAnonymousTypeWorker(node: TypeLiteralNode | MappedTypeNode | JSDocTypeLiteral | JSDocRecordType) {
return bindAnonymousDeclaration(<Declaration>node, SymbolFlags.TypeLiteral, "__type");
}

function checkTypePredicate(node: TypePredicateNode) {
const { parameterName, type } = node;
if (parameterName && parameterName.kind === SyntaxKind.Identifier) {
Expand Down Expand Up @@ -2558,10 +2576,8 @@ namespace ts {
}

function bindPropertyOrMethodOrAccessor(node: Declaration, symbolFlags: SymbolFlags, symbolExcludes: SymbolFlags) {
if (!file.isDeclarationFile && !isInAmbientContext(node)) {
if (isAsyncFunction(node)) {
emitFlags |= NodeFlags.HasAsyncFunctions;
}
if (!file.isDeclarationFile && !isInAmbientContext(node) && isAsyncFunction(node)) {
emitFlags |= NodeFlags.HasAsyncFunctions;
}

if (currentFlow && isObjectLiteralOrClassExpressionMethod(node)) {
Expand All @@ -2573,10 +2589,6 @@ namespace ts {
: declareSymbolAndAddToSymbolTable(node, symbolFlags, symbolExcludes);
}

function bindJSDocProperty(node: JSDocPropertyTag) {
return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Property, SymbolFlags.PropertyExcludes);
}

// reachability checks

function shouldReportErrorOnModuleDeclaration(node: ModuleDeclaration): boolean {
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22320,6 +22320,11 @@ namespace ts {
}
}

if (entityName.parent!.kind === SyntaxKind.JSDocParameterTag) {
const parameter = ts.getParameterFromJSDoc(entityName.parent as JSDocParameterTag);
return parameter && parameter.symbol;
}

if (isPartOfExpression(entityName)) {
if (nodeIsMissing(entityName)) {
// Missing entity name.
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ namespace ts {
// stored in properties. If a 'cbNodes' callback is specified, it is invoked for embedded arrays; otherwise,
// embedded arrays are flattened and the 'cbNode' callback is invoked for each element. If a callback returns
// a truthy value, iteration stops and that value is returned. Otherwise, undefined is returned.
export function forEachChild<T>(node: Node, cbNode: (node: Node) => T, cbNodeArray?: (nodes: Node[]) => T): T | undefined {
export function forEachChild<T>(node: Node, cbNode: (node: Node) => T | undefined, cbNodeArray?: (nodes: NodeArray<Node>) => T | undefined): T | undefined {
if (!node) {
return;
}
Expand Down
23 changes: 16 additions & 7 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,7 +1599,7 @@ namespace ts {

// Pull parameter comments from declaring function as well
if (node.kind === SyntaxKind.Parameter) {
cache = concatenate(cache, getJSDocParameterTags(node));
cache = concatenate(cache, getJSDocParameterTags(node as ParameterDeclaration));
}

if (isVariableLike(node) && node.initializer) {
Expand All @@ -1610,11 +1610,8 @@ namespace ts {
}
}

export function getJSDocParameterTags(param: Node): JSDocParameterTag[] {
if (!isParameter(param)) {
return undefined;
}
const func = param.parent as FunctionLikeDeclaration;
export function getJSDocParameterTags(param: ParameterDeclaration): JSDocParameterTag[] {
const func = param.parent;
const tags = getJSDocTags(func, SyntaxKind.JSDocParameterTag) as JSDocParameterTag[];
if (!param.name) {
// this is an anonymous jsdoc param from a `function(type1, type2): type3` specification
Expand All @@ -1635,10 +1632,22 @@ namespace ts {
}
}

/** Does the opposite of `getJSDocParameterTags`: given a JSDoc parameter, finds the parameter corresponding to it. */
export function getParameterFromJSDoc(node: JSDocParameterTag): ParameterDeclaration | undefined {
const name = node.parameterName.text;
const grandParent = node.parent!.parent!;
Debug.assert(node.parent!.kind === SyntaxKind.JSDocComment);
if (!isFunctionLike(grandParent)) {
return undefined;
}
return find(grandParent.parameters, p =>
p.name.kind === SyntaxKind.Identifier && p.name.text === name);
}

export function getJSDocType(node: Node): JSDocType {
let tag: JSDocTypeTag | JSDocParameterTag = getFirstJSDocTag(node, SyntaxKind.JSDocTypeTag) as JSDocTypeTag;
if (!tag && node.kind === SyntaxKind.Parameter) {
const paramTags = getJSDocParameterTags(node);
const paramTags = getJSDocParameterTags(node as ParameterDeclaration);
if (paramTags) {
tag = find(paramTags, tag => !!tag.typeExpression);
}
Expand Down
9 changes: 6 additions & 3 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,7 @@ namespace FourSlash {
}

private getNode(): ts.Node {
return ts.getTouchingPropertyName(this.getSourceFile(), this.currentCaretPosition);
return ts.getTouchingPropertyName(this.getSourceFile(), this.currentCaretPosition, /*includeJsDocComment*/ false);
}

private goToAndGetNode(range: Range): ts.Node {
Expand Down Expand Up @@ -994,12 +994,11 @@ namespace FourSlash {
}

public verifyReferenceGroups(startRanges: Range | Range[], parts: Array<{ definition: string, ranges: Range[] }>): void {
interface ReferenceJson { definition: string; ranges: ts.ReferenceEntry[]; }
const fullExpected = ts.map(parts, ({ definition, ranges }) => ({ definition, ranges: ranges.map(rangeToReferenceEntry) }));

for (const startRange of toArray(startRanges)) {
this.goToRangeStart(startRange);
const fullActual = ts.map<ts.ReferencedSymbol, ReferenceJson>(this.findReferencesAtCaret(), ({ definition, references }) => ({
const fullActual = ts.map(this.findReferencesAtCaret(), ({ definition, references }) => ({
definition: definition.displayParts.map(d => d.text).join(""),
ranges: references
}));
Expand Down Expand Up @@ -1046,6 +1045,10 @@ namespace FourSlash {
this.raiseError(`${msgPrefix}At ${path}: ${msg}`);
};

if ((actual === undefined) !== (expected === undefined)) {
fail(`Expected ${expected}, got ${actual}`);
}

for (const key in actual) if (ts.hasProperty(actual as any, key)) {
const ak = actual[key], ek = expected[key];
if (typeof ak === "object" && typeof ek === "object") {
Expand Down
4 changes: 2 additions & 2 deletions src/harness/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ namespace Utils {
assert.isFalse(child.pos < currentPos, "child.pos < currentPos");
currentPos = child.end;
},
(array: ts.NodeArray<ts.Node>) => {
array => {
assert.isFalse(array.pos < node.pos, "array.pos < node.pos");
assert.isFalse(array.end > node.end, "array.end > node.end");
assert.isFalse(array.pos < currentPos, "array.pos < currentPos");
Expand Down Expand Up @@ -383,7 +383,7 @@ namespace Utils {

assertStructuralEquals(child1, child2);
},
(array1: ts.NodeArray<ts.Node>) => {
array1 => {
const childName = findChildName(node1, array1);
const array2: ts.NodeArray<ts.Node> = (<any>node2)[childName];

Expand Down
2 changes: 1 addition & 1 deletion src/services/breakpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace ts.BreakpointResolver {
return undefined;
}

let tokenAtLocation = getTokenAtPosition(sourceFile, position);
let tokenAtLocation = getTokenAtPosition(sourceFile, position, /*includeJsDocComment*/ false);
const lineOfPosition = sourceFile.getLineAndCharacterOfPosition(position).line;
if (sourceFile.getLineAndCharacterOfPosition(tokenAtLocation.getStart(sourceFile)).line > lineOfPosition) {
// Get previous token if the token is returned starts on new line
Expand Down
2 changes: 1 addition & 1 deletion src/services/codefixes/disableJsDiagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ namespace ts.codefix {
// We also want to check if the previous line holds a comment for a node on the next line
// if so, we do not want to separate the node from its comment if we can.
if (!isInComment(sourceFile, startPosition) && !isInString(sourceFile, startPosition) && !isInTemplateString(sourceFile, startPosition)) {
const token = getTouchingToken(sourceFile, startPosition);
const token = getTouchingToken(sourceFile, startPosition, /*includeJsDocComment*/ false);
const tokenLeadingCommnets = getLeadingCommentRangesOfNode(token, sourceFile);
if (!tokenLeadingCommnets || !tokenLeadingCommnets.length || tokenLeadingCommnets[0].pos >= startPosition) {
return {
Expand Down
2 changes: 1 addition & 1 deletion src/services/codefixes/fixAddMissingMember.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace ts.codefix {
// This is the identifier of the missing property. eg:
// this.missing = 1;
// ^^^^^^^
const token = getTokenAtPosition(sourceFile, start);
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);

if (token.kind !== SyntaxKind.Identifier) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace ts.codefix {
const start = context.span.start;
// This is the identifier in the case of a class declaration
// or the class keyword token in the case of a class expression.
const token = getTokenAtPosition(sourceFile, start);
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);
const checker = context.program.getTypeChecker();

if (isClassLike(token.parent)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace ts.codefix {
function getActionForClassLikeIncorrectImplementsInterface(context: CodeFixContext): CodeAction[] | undefined {
const sourceFile = context.sourceFile;
const start = context.span.start;
const token = getTokenAtPosition(sourceFile, start);
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);
const checker = context.program.getTypeChecker();

const classDeclaration = getContainingClass(token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace ts.codefix {
getCodeActions: (context: CodeFixContext) => {
const sourceFile = context.sourceFile;

const token = getTokenAtPosition(sourceFile, context.span.start);
const token = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
if (token.kind !== SyntaxKind.ThisKeyword) {
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace ts.codefix {
errorCodes: [Diagnostics.Constructors_for_derived_classes_must_contain_a_super_call.code],
getCodeActions: (context: CodeFixContext) => {
const sourceFile = context.sourceFile;
const token = getTokenAtPosition(sourceFile, context.span.start);
const token = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);

if (token.kind !== SyntaxKind.ConstructorKeyword) {
return undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace ts.codefix {
getCodeActions: (context: CodeFixContext) => {
const sourceFile = context.sourceFile;
const start = context.span.start;
const token = getTokenAtPosition(sourceFile, start);
const token = getTokenAtPosition(sourceFile, start, /*includeJsDocComment*/ false);
const classDeclNode = getContainingClass(token);
if (!(token.kind === SyntaxKind.Identifier && isClassLike(classDeclNode))) {
return undefined;
Expand Down
2 changes: 1 addition & 1 deletion src/services/codefixes/fixForgottenThisPropertyAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace ts.codefix {
errorCodes: [Diagnostics.Cannot_find_name_0_Did_you_mean_the_instance_member_this_0.code],
getCodeActions: (context: CodeFixContext) => {
const sourceFile = context.sourceFile;
const token = getTokenAtPosition(sourceFile, context.span.start);
const token = getTokenAtPosition(sourceFile, context.span.start, /*includeJsDocComment*/ false);
if (token.kind !== SyntaxKind.Identifier) {
return undefined;
}
Expand Down
Loading