From d6a3b6c28292358983d944018947af27e0b4daf2 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Tue, 10 Apr 2018 10:20:34 -0700 Subject: [PATCH 1/3] JSDoc:positional matching of destructured params 1. When looking up tags for a parameter whose name is a binding pattern, use the index of the parameter to get the type. 2. When reporting errors for `@param` tags with no matching parameter name, do not report the error for tags whose index in the `@param` tag list matches the index of a parameter whose name is a binding pattern. --- src/compiler/checker.ts | 4 + src/compiler/utilities.ts | 19 ++- .../reference/jsdocParamTag2.errors.txt | 82 ++++++++++++ .../reference/jsdocParamTag2.symbols | 117 ++++++++++++++++++ .../baselines/reference/jsdocParamTag2.types | 117 ++++++++++++++++++ .../cases/conformance/jsdoc/jsdocParamTag2.ts | 71 +++++++++++ 6 files changed, 406 insertions(+), 4 deletions(-) create mode 100644 tests/baselines/reference/jsdocParamTag2.errors.txt create mode 100644 tests/baselines/reference/jsdocParamTag2.symbols create mode 100644 tests/baselines/reference/jsdocParamTag2.types create mode 100644 tests/cases/conformance/jsdoc/jsdocParamTag2.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 5634e06d5fd2b..42e7e8f7b16f8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -21871,6 +21871,10 @@ namespace ts { // and give a better error message when the host function mentions `arguments` // but the tag doesn't have an array type if (decl) { + const i = getJSDocTags(decl).filter(isJSDocParameterTag).indexOf(node); + if (i > -1 && i < decl.parameters.length && isBindingPattern(decl.parameters[i].name)) { + return; + } if (!containsArgumentsReference(decl)) { error(node.name, Diagnostics.JSDoc_param_tag_has_name_0_but_there_is_no_parameter_with_that_name, diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 8e6b3f8ae3972..e2ad870d43029 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4632,11 +4632,22 @@ namespace ts { * parameters by name and binding patterns do not have a name. */ export function getJSDocParameterTags(param: ParameterDeclaration): ReadonlyArray { - if (param.name && isIdentifier(param.name)) { - const name = param.name.escapedText; - return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && isIdentifier(tag.name) && tag.name.escapedText === name); + if (param.name) { + if (isIdentifier(param.name)) { + const name = param.name.escapedText; + return getJSDocTags(param.parent).filter((tag): tag is JSDocParameterTag => isJSDocParameterTag(tag) && isIdentifier(tag.name) && tag.name.escapedText === name); + } + else { + const i = param.parent.parameters.indexOf(param); + if (i > -1) { + const paramTags = getJSDocTags(param.parent).filter(isJSDocParameterTag); + if (i < paramTags.length) { + return [paramTags[i]]; + } + } + } } - // a binding pattern doesn't have a name, so it's not possible to match it a JSDoc parameter, which is identified by name + // return empty array for: incorrect binding patterns and JSDoc function syntax, which has un-named parameters return emptyArray; } diff --git a/tests/baselines/reference/jsdocParamTag2.errors.txt b/tests/baselines/reference/jsdocParamTag2.errors.txt new file mode 100644 index 0000000000000..e19140b3f9deb --- /dev/null +++ b/tests/baselines/reference/jsdocParamTag2.errors.txt @@ -0,0 +1,82 @@ +tests/cases/conformance/jsdoc/0.js(56,20): error TS8024: JSDoc '@param' tag has name 'obj', but there is no parameter with that name. +tests/cases/conformance/jsdoc/0.js(61,19): error TS2459: Type 'string' has no property 'a' and no string index signature. +tests/cases/conformance/jsdoc/0.js(61,22): error TS2459: Type 'string' has no property 'b' and no string index signature. +tests/cases/conformance/jsdoc/0.js(63,20): error TS8024: JSDoc '@param' tag has name 'y', but there is no parameter with that name. + + +==== tests/cases/conformance/jsdoc/0.js (4 errors) ==== + // Object literal syntax + /** + * @param {{a: string, b: string}} obj + * @param {string} x + */ + function good1({a, b}, x) {} + /** + * @param {{a: string, b: string}} obj + * @param {{c: number, d: number}} OBJECTION + */ + function good2({a, b}, {c, d}) {} + /** + * @param {number} x + * @param {{a: string, b: string}} obj + * @param {string} y + */ + function good3(x, {a, b}, y) {} + /** + * @param {{a: string, b: string}} obj + */ + function good4({a, b}) {} + + // nested object syntax + /** + * @param {Object} obj + * @param {string} obj.a - this is like the saddest way to specify a type + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {string} x + */ + function good5({a, b}, x) {} + /** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {Object} OBJECTION - documentation here too + * @param {string} OBJECTION.c + * @param {string} OBJECTION.d - meh + */ + function good6({a, b}, {c, d}) {} + /** + * @param {number} x + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + * @param {string} y + */ + function good7(x, {a, b}, y) {} + /** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + */ + function good8({a, b}) {} + + /** + * @param {object} obj - this type gets ignored + ~~~ +!!! error TS8024: JSDoc '@param' tag has name 'obj', but there is no parameter with that name. + * @param {string} obj.a + * @param {string} obj.b - and x's type gets used for both parameters + * @param {string} x + */ + function bad1(x, {a, b}) {} + ~ +!!! error TS2459: Type 'string' has no property 'a' and no string index signature. + ~ +!!! error TS2459: Type 'string' has no property 'b' and no string index signature. + /** + * @param {string} y - here, y's type gets ignored but obj's is fine + ~ +!!! error TS8024: JSDoc '@param' tag has name 'y', but there is no parameter with that name. + * @param {{a: string, b: string}} obj + */ + function bad2(x, {a, b}) {} + \ No newline at end of file diff --git a/tests/baselines/reference/jsdocParamTag2.symbols b/tests/baselines/reference/jsdocParamTag2.symbols new file mode 100644 index 0000000000000..15eea8946e19d --- /dev/null +++ b/tests/baselines/reference/jsdocParamTag2.symbols @@ -0,0 +1,117 @@ +=== tests/cases/conformance/jsdoc/0.js === +// Object literal syntax +/** + * @param {{a: string, b: string}} obj + * @param {string} x + */ +function good1({a, b}, x) {} +>good1 : Symbol(good1, Decl(0.js, 0, 0)) +>a : Symbol(a, Decl(0.js, 5, 16)) +>b : Symbol(b, Decl(0.js, 5, 18)) +>x : Symbol(x, Decl(0.js, 5, 22)) + +/** + * @param {{a: string, b: string}} obj + * @param {{c: number, d: number}} OBJECTION + */ +function good2({a, b}, {c, d}) {} +>good2 : Symbol(good2, Decl(0.js, 5, 28)) +>a : Symbol(a, Decl(0.js, 10, 16)) +>b : Symbol(b, Decl(0.js, 10, 18)) +>c : Symbol(c, Decl(0.js, 10, 24)) +>d : Symbol(d, Decl(0.js, 10, 26)) + +/** + * @param {number} x + * @param {{a: string, b: string}} obj + * @param {string} y + */ +function good3(x, {a, b}, y) {} +>good3 : Symbol(good3, Decl(0.js, 10, 33)) +>x : Symbol(x, Decl(0.js, 16, 15)) +>a : Symbol(a, Decl(0.js, 16, 19)) +>b : Symbol(b, Decl(0.js, 16, 21)) +>y : Symbol(y, Decl(0.js, 16, 25)) + +/** + * @param {{a: string, b: string}} obj + */ +function good4({a, b}) {} +>good4 : Symbol(good4, Decl(0.js, 16, 31)) +>a : Symbol(a, Decl(0.js, 20, 16)) +>b : Symbol(b, Decl(0.js, 20, 18)) + +// nested object syntax +/** + * @param {Object} obj + * @param {string} obj.a - this is like the saddest way to specify a type + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {string} x + */ +function good5({a, b}, x) {} +>good5 : Symbol(good5, Decl(0.js, 20, 25)) +>a : Symbol(a, Decl(0.js, 29, 16)) +>b : Symbol(b, Decl(0.js, 29, 18)) +>x : Symbol(x, Decl(0.js, 29, 22)) + +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {Object} OBJECTION - documentation here too + * @param {string} OBJECTION.c + * @param {string} OBJECTION.d - meh + */ +function good6({a, b}, {c, d}) {} +>good6 : Symbol(good6, Decl(0.js, 29, 28)) +>a : Symbol(a, Decl(0.js, 38, 16)) +>b : Symbol(b, Decl(0.js, 38, 18)) +>c : Symbol(c, Decl(0.js, 38, 24)) +>d : Symbol(d, Decl(0.js, 38, 26)) + +/** + * @param {number} x + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + * @param {string} y + */ +function good7(x, {a, b}, y) {} +>good7 : Symbol(good7, Decl(0.js, 38, 33)) +>x : Symbol(x, Decl(0.js, 46, 15)) +>a : Symbol(a, Decl(0.js, 46, 19)) +>b : Symbol(b, Decl(0.js, 46, 21)) +>y : Symbol(y, Decl(0.js, 46, 25)) + +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + */ +function good8({a, b}) {} +>good8 : Symbol(good8, Decl(0.js, 46, 31)) +>a : Symbol(a, Decl(0.js, 52, 16)) +>b : Symbol(b, Decl(0.js, 52, 18)) + +/** + * @param {object} obj - this type gets ignored + * @param {string} obj.a + * @param {string} obj.b - and x's type gets used for both parameters + * @param {string} x + */ +function bad1(x, {a, b}) {} +>bad1 : Symbol(bad1, Decl(0.js, 52, 25)) +>x : Symbol(x, Decl(0.js, 60, 14)) +>a : Symbol(a, Decl(0.js, 60, 18)) +>b : Symbol(b, Decl(0.js, 60, 20)) + +/** + * @param {string} y - here, y's type gets ignored but obj's is fine + * @param {{a: string, b: string}} obj + */ +function bad2(x, {a, b}) {} +>bad2 : Symbol(bad2, Decl(0.js, 60, 27)) +>x : Symbol(x, Decl(0.js, 65, 14)) +>a : Symbol(a, Decl(0.js, 65, 18)) +>b : Symbol(b, Decl(0.js, 65, 20)) + diff --git a/tests/baselines/reference/jsdocParamTag2.types b/tests/baselines/reference/jsdocParamTag2.types new file mode 100644 index 0000000000000..a303403f0b935 --- /dev/null +++ b/tests/baselines/reference/jsdocParamTag2.types @@ -0,0 +1,117 @@ +=== tests/cases/conformance/jsdoc/0.js === +// Object literal syntax +/** + * @param {{a: string, b: string}} obj + * @param {string} x + */ +function good1({a, b}, x) {} +>good1 : ({ a, b }: { a: string; b: string; }, x: string) => void +>a : string +>b : string +>x : string + +/** + * @param {{a: string, b: string}} obj + * @param {{c: number, d: number}} OBJECTION + */ +function good2({a, b}, {c, d}) {} +>good2 : ({ a, b }: { a: string; b: string; }, { c, d }: { c: number; d: number; }) => void +>a : string +>b : string +>c : number +>d : number + +/** + * @param {number} x + * @param {{a: string, b: string}} obj + * @param {string} y + */ +function good3(x, {a, b}, y) {} +>good3 : (x: number, { a, b }: { a: string; b: string; }, y: string) => void +>x : number +>a : string +>b : string +>y : string + +/** + * @param {{a: string, b: string}} obj + */ +function good4({a, b}) {} +>good4 : ({ a, b }: { a: string; b: string; }) => void +>a : string +>b : string + +// nested object syntax +/** + * @param {Object} obj + * @param {string} obj.a - this is like the saddest way to specify a type + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {string} x + */ +function good5({a, b}, x) {} +>good5 : ({ a, b }: { a: string; b: string; }, x: string) => void +>a : string +>b : string +>x : string + +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {Object} OBJECTION - documentation here too + * @param {string} OBJECTION.c + * @param {string} OBJECTION.d - meh + */ +function good6({a, b}, {c, d}) {} +>good6 : ({ a, b }: { a: string; b: string; }, { c, d }: { c: string; d: string; }) => void +>a : string +>b : string +>c : string +>d : string + +/** + * @param {number} x + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + * @param {string} y + */ +function good7(x, {a, b}, y) {} +>good7 : (x: number, { a, b }: { a: string; b: string; }, y: string) => void +>x : number +>a : string +>b : string +>y : string + +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + */ +function good8({a, b}) {} +>good8 : ({ a, b }: { a: string; b: string; }) => void +>a : string +>b : string + +/** + * @param {object} obj - this type gets ignored + * @param {string} obj.a + * @param {string} obj.b - and x's type gets used for both parameters + * @param {string} x + */ +function bad1(x, {a, b}) {} +>bad1 : (x: string, { a, b }: string) => void +>x : string +>a : any +>b : any + +/** + * @param {string} y - here, y's type gets ignored but obj's is fine + * @param {{a: string, b: string}} obj + */ +function bad2(x, {a, b}) {} +>bad2 : (x: any, { a, b }: { a: string; b: string; }) => void +>x : any +>a : string +>b : string + diff --git a/tests/cases/conformance/jsdoc/jsdocParamTag2.ts b/tests/cases/conformance/jsdoc/jsdocParamTag2.ts new file mode 100644 index 0000000000000..e9ebbe050c8d0 --- /dev/null +++ b/tests/cases/conformance/jsdoc/jsdocParamTag2.ts @@ -0,0 +1,71 @@ +// @allowJS: true +// @checkJS: true +// @noEmit: true +// @Filename: 0.js + +// Object literal syntax +/** + * @param {{a: string, b: string}} obj + * @param {string} x + */ +function good1({a, b}, x) {} +/** + * @param {{a: string, b: string}} obj + * @param {{c: number, d: number}} OBJECTION + */ +function good2({a, b}, {c, d}) {} +/** + * @param {number} x + * @param {{a: string, b: string}} obj + * @param {string} y + */ +function good3(x, {a, b}, y) {} +/** + * @param {{a: string, b: string}} obj + */ +function good4({a, b}) {} + +// nested object syntax +/** + * @param {Object} obj + * @param {string} obj.a - this is like the saddest way to specify a type + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {string} x + */ +function good5({a, b}, x) {} +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b - but it sure does allow a lot of documentation + * @param {Object} OBJECTION - documentation here too + * @param {string} OBJECTION.c + * @param {string} OBJECTION.d - meh + */ +function good6({a, b}, {c, d}) {} +/** + * @param {number} x + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + * @param {string} y + */ +function good7(x, {a, b}, y) {} +/** + * @param {Object} obj + * @param {string} obj.a + * @param {string} obj.b + */ +function good8({a, b}) {} + +/** + * @param {object} obj - this type gets ignored + * @param {string} obj.a + * @param {string} obj.b - and x's type gets used for both parameters + * @param {string} x + */ +function bad1(x, {a, b}) {} +/** + * @param {string} y - here, y's type gets ignored but obj's is fine + * @param {{a: string, b: string}} obj + */ +function bad2(x, {a, b}) {} From 9f01f81426073397a1f37b89c3529984df09ccbc Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Tue, 10 Apr 2018 11:20:39 -0700 Subject: [PATCH 2/3] Change to an assert --- src/compiler/utilities.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index e2ad870d43029..aa01cb67643f7 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4639,11 +4639,10 @@ namespace ts { } else { const i = param.parent.parameters.indexOf(param); - if (i > -1) { - const paramTags = getJSDocTags(param.parent).filter(isJSDocParameterTag); - if (i < paramTags.length) { - return [paramTags[i]]; - } + Debug.assert(i > -1, "Parameters should always be in their parents' parameter list"); + const paramTags = getJSDocTags(param.parent).filter(isJSDocParameterTag); + if (i < paramTags.length) { + return [paramTags[i]]; } } } From 0fdc12d9ae416036b0b8c1956311aaa883c42508 Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Tue, 10 Apr 2018 12:33:08 -0700 Subject: [PATCH 3/3] Improve comment text --- src/compiler/utilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index aa01cb67643f7..b1912192685aa 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -4646,7 +4646,7 @@ namespace ts { } } } - // return empty array for: incorrect binding patterns and JSDoc function syntax, which has un-named parameters + // return empty array for: out-of-order binding patterns and JSDoc function syntax, which has un-named parameters return emptyArray; }