[labs/analyzer] Support const function/class declarations#3662
[labs/analyzer] Support const function/class declarations#3662kevinpschaaf merged 6 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: c2f29ed The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
📊 Tachometer Benchmark ResultsSummarynop-update
render
update
update-reflect
Resultslit-element-list
render
update
update-reflect
lit-html-kitchen-sink
render
update
nop-update
lit-html-repeat
render
update
lit-html-template-heavy
render
update
reactive-element-list
render
update
update-reflect
|
| test('tagged description and summary', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('TaggedDescription'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| assert.equal( | ||
| dec.description, | ||
| `TaggedDescription description. Lorem ipsum dolor sit amet, consectetur | ||
| adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna | ||
| aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris | ||
| nisi ut aliquip ex ea commodo consequat.` | ||
| ); | ||
| assert.equal(dec.summary, `TaggedDescription summary.`); | ||
| assert.equal(dec.deprecated, `TaggedDescription deprecated message.`); | ||
| }); | ||
|
|
||
| test('untagged description', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('UntaggedDescription'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| assert.equal( | ||
| dec.description, | ||
| `UntaggedDescription description. Lorem ipsum dolor sit amet, consectetur | ||
| adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna | ||
| aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris | ||
| nisi ut aliquip ex ea commodo consequat.` | ||
| ); | ||
| assert.equal(dec.summary, `UntaggedDescription summary.`); | ||
| assert.equal(dec.deprecated, `UntaggedDescription deprecated message.`); | ||
| }); | ||
|
|
||
| // Fields | ||
|
|
||
| test('field1', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getField('field1'); | ||
| assert.ok(member?.isClassField()); | ||
| assert.equal( | ||
| member.description, | ||
| `Class field 1 description\nwith wraparound` | ||
| ); | ||
| assert.equal(member.default, `'default1'`); | ||
| assert.equal(member.privacy, 'private'); | ||
| assert.equal(member.type?.text, 'string'); | ||
| }); | ||
|
|
||
| test('field2', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getField('field2'); | ||
| assert.ok(member?.isClassField()); | ||
| assert.equal(member.summary, `Class field 2 summary\nwith wraparound`); | ||
| assert.equal( | ||
| member.description, | ||
| `Class field 2 description\nwith wraparound` | ||
| ); | ||
| assert.equal(member.default, undefined); | ||
| assert.equal(member.privacy, 'protected'); | ||
| assert.equal(member.type?.text, 'string | number'); | ||
| }); | ||
|
|
||
| test('field3', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getField('field3'); | ||
| assert.ok(member?.isClassField()); | ||
| assert.equal( | ||
| member.description, | ||
| `Class field 3 description\nwith wraparound` | ||
| ); | ||
| assert.equal(member.default, undefined); | ||
| assert.equal(member.privacy, 'public'); | ||
| assert.equal(member.type?.text, 'string'); | ||
| assert.equal(member.deprecated, true); | ||
| }); | ||
|
|
||
| test('field4', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getField('field4'); | ||
| assert.ok(member?.isClassField()); | ||
| assert.equal(member.summary, `Class field 4 summary\nwith wraparound`); | ||
| assert.equal( | ||
| member.description, | ||
| `Class field 4 description\nwith wraparound` | ||
| ); | ||
| assert.equal( | ||
| member.default, | ||
| `new Promise${lang === 'ts' ? '<void>' : ''}((r) => r())` | ||
| ); | ||
| assert.equal(member.type?.text, 'Promise<void>'); | ||
| assert.equal(member.deprecated, 'Class field 4 deprecated'); | ||
| }); | ||
|
|
||
| test('static field1', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getStaticField('field1'); | ||
| assert.ok(member?.isClassField()); | ||
| assert.equal(member.summary, `Static class field 1 summary`); | ||
| assert.equal(member.description, `Static class field 1 description`); | ||
| assert.equal(member.default, undefined); | ||
| assert.equal(member.privacy, 'protected'); | ||
| assert.equal(member.type?.text, 'string | number'); | ||
| }); | ||
|
|
||
| // Methods | ||
|
|
||
| test('method1', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getMethod('method1'); | ||
| assert.ok(member?.isClassMethod()); | ||
| assert.equal(member.description, `Method 1 description\nwith wraparound`); | ||
| assert.equal(member.parameters?.length, 0); | ||
| assert.equal(member.return?.type?.text, 'void'); | ||
| }); | ||
|
|
||
| test('method2', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getMethod('method2'); | ||
| assert.ok(member?.isClassMethod()); | ||
| assert.equal(member.summary, `Method 2 summary\nwith wraparound`); | ||
| assert.equal(member.description, `Method 2 description\nwith wraparound`); | ||
| assert.equal(member.parameters?.length, 3); | ||
| assert.equal(member.parameters?.[0].name, 'a'); | ||
| assert.equal(member.parameters?.[0].description, 'Param a description'); | ||
| assert.equal(member.parameters?.[0].summary, undefined); | ||
| assert.equal(member.parameters?.[0].type?.text, 'string'); | ||
| assert.equal(member.parameters?.[0].default, undefined); | ||
| assert.equal(member.parameters?.[0].rest, false); | ||
| assert.equal(member.parameters?.[1].name, 'b'); | ||
| assert.equal( | ||
| member.parameters?.[1].description, | ||
| 'Param b description\nwith wraparound' | ||
| ); | ||
| assert.equal(member.parameters?.[1].type?.text, 'boolean'); | ||
| assert.equal(member.parameters?.[1].optional, true); | ||
| assert.equal(member.parameters?.[1].default, 'false'); | ||
| assert.equal(member.parameters?.[1].rest, false); | ||
| assert.equal(member.parameters?.[2].name, 'c'); | ||
| assert.equal(member.parameters?.[2].description, 'Param c description'); | ||
| assert.equal(member.parameters?.[2].summary, undefined); | ||
| assert.equal(member.parameters?.[2].type?.text, 'number[]'); | ||
| assert.equal(member.parameters?.[2].optional, false); | ||
| assert.equal(member.parameters?.[2].default, undefined); | ||
| assert.equal(member.parameters?.[2].rest, true); | ||
| assert.equal(member.return?.type?.text, 'string'); | ||
| assert.equal(member.return?.description, 'Method 2 return description'); | ||
| assert.equal(member.deprecated, 'Method 2 deprecated'); | ||
| }); | ||
|
|
||
| test('static method1', ({getModule}) => { | ||
| const dec = getModule('classes').getDeclaration('Class1'); | ||
| assert.ok(dec.isClassDeclaration()); | ||
| const member = dec.getStaticMethod('method1'); | ||
| assert.ok(member?.isClassMethod()); | ||
| assert.equal(member.summary, `Static method 1 summary`); | ||
| assert.equal(member.description, `Static method 1 description`); | ||
| assert.equal(member.parameters?.length, 3); | ||
| assert.equal(member.parameters?.[0].name, 'a'); | ||
| assert.equal(member.parameters?.[0].description, 'Param a description'); | ||
| assert.equal(member.parameters?.[0].summary, undefined); | ||
| assert.equal(member.parameters?.[0].type?.text, 'string'); | ||
| assert.equal(member.parameters?.[0].default, undefined); | ||
| assert.equal(member.parameters?.[0].rest, false); | ||
| assert.equal(member.parameters?.[1].name, 'b'); | ||
| assert.equal(member.parameters?.[1].description, 'Param b description'); | ||
| assert.equal(member.parameters?.[1].type?.text, 'boolean'); | ||
| assert.equal(member.parameters?.[1].optional, true); | ||
| assert.equal(member.parameters?.[1].default, 'false'); | ||
| assert.equal(member.parameters?.[1].rest, false); | ||
| assert.equal(member.parameters?.[2].name, 'c'); | ||
| assert.equal(member.parameters?.[2].description, 'Param c description'); | ||
| assert.equal(member.parameters?.[2].summary, undefined); | ||
| assert.equal(member.parameters?.[2].type?.text, 'number[]'); | ||
| assert.equal(member.parameters?.[2].optional, false); | ||
| assert.equal(member.parameters?.[2].default, undefined); | ||
| assert.equal(member.parameters?.[2].rest, true); | ||
| assert.equal(member.return?.type?.text, 'string'); | ||
| assert.equal( | ||
| member.return?.description, | ||
| 'Static method 1 return description' | ||
| ); | ||
| assert.equal(member.deprecated, 'Static method 1 deprecated'); | ||
| }); |
There was a problem hiding this comment.
All of these tests are moved unchanged from lit-element/jsdoc_test.js (and their duplicates that were in vanilla-element/jsdoc_test.js into here, so that this file is now all of the class-related tests.
The new tests are the const tests below, plus the superClass test, so that we have a superClass test in the basic class tests (there is another in the LitElement tests, so there was some coverage, but this makes sure it works for vanilla classes also).
| // Doing module JSDoc tests in-memory, to test a number of variations | ||
| // without needing to maintain a file for each. | ||
|
|
||
| for (const hasFirstStatementDoc of [false, true]) { | ||
| const moduleTest = suite<{ | ||
| analyzer: InMemoryAnalyzer; | ||
| }>( | ||
| `Module jsDoc tests, ${ | ||
| hasFirstStatementDoc ? 'has' : 'no' | ||
| } first statement docs (${lang})` | ||
| ); | ||
|
|
||
| moduleTest.before.each((ctx) => { | ||
| ctx.analyzer = new InMemoryAnalyzer(lang, { | ||
| '/package.json': JSON.stringify({name: '@lit-internal/in-memory-test'}), | ||
| }); | ||
| }); | ||
|
|
||
| const firstStatementDoc = hasFirstStatementDoc | ||
| ? ` | ||
| /** | ||
| * First statement description | ||
| * @summary First statement summary | ||
| */ | ||
| ` | ||
| : ''; | ||
|
|
||
| moduleTest('untagged module description with @module tag', ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more description | ||
| * @module | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal(module.description, 'Module description\nmore description'); | ||
| }); | ||
|
|
||
| moduleTest( | ||
| 'untagged module description with @fileoverview tag', | ||
| ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more description | ||
| * @fileoverview | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore description' | ||
| ); | ||
| } | ||
| ); | ||
|
|
||
| moduleTest('module description in @fileoverview tag', ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * @fileoverview Module description | ||
| * more description | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal(module.description, 'Module description\nmore description'); | ||
| }); | ||
|
|
||
| moduleTest( | ||
| 'untagged module description with @packageDocumentation tag', | ||
| ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more description | ||
| * @packageDocumentation | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore description' | ||
| ); | ||
| } | ||
| ); | ||
|
|
||
| moduleTest( | ||
| 'module description in @packageDocumentation tag', | ||
| ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * @packageDocumentation Module description | ||
| * more description | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore description' | ||
| ); | ||
| } | ||
| ); | ||
|
|
||
| moduleTest( | ||
| 'module description in @packageDocumentation tag with other tags', | ||
| ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * @packageDocumentation Module description | ||
| * more description | ||
| * @module foo | ||
| * @deprecated Module is deprecated | ||
| */ | ||
| ${firstStatementDoc} | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore description' | ||
| ); | ||
| assert.equal(module.deprecated, 'Module is deprecated'); | ||
| } | ||
| ); | ||
|
|
||
| moduleTest('untagged module description', ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more module description | ||
| * @summary Module summary | ||
| * @deprecated | ||
| */ | ||
| /** | ||
| * First statement description | ||
| * @summary First statement summary | ||
| */ | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore module description' | ||
| ); | ||
| assert.equal(module.summary, 'Module summary'); | ||
| assert.equal(module.deprecated, true); | ||
| }); | ||
|
|
||
| moduleTest('multiple untagged module descriptions', ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more module description | ||
| */ | ||
| /** | ||
| * Even more module description | ||
| */ | ||
| /** | ||
| * First statement description | ||
| * @summary First statement summary | ||
| */ | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore module description\nEven more module description' | ||
| ); | ||
| }); | ||
|
|
||
| moduleTest( | ||
| 'multiple untagged module descriptions with other tags', | ||
| ({analyzer}) => { | ||
| analyzer.setFile( | ||
| '/module', | ||
| ` | ||
| /** | ||
| * Module description | ||
| * more module description | ||
| * @deprecated | ||
| */ | ||
| /** | ||
| * Even more module description | ||
| * @summary Module summary | ||
| */ | ||
| /** | ||
| * First statement description | ||
| * @summary First statement summary | ||
| */ | ||
| export const foo = 42; | ||
| ` | ||
| ); | ||
| const module = analyzer.getModule( | ||
| getSourceFilename('/module', lang) as AbsolutePath | ||
| ); | ||
| assert.equal( | ||
| module.description, | ||
| 'Module description\nmore module description\nEven more module description' | ||
| ); | ||
| assert.equal(module.summary, 'Module summary'); | ||
| assert.equal(module.deprecated, true); | ||
| } | ||
| ); |
There was a problem hiding this comment.
Moved from lit-element/jsdoc_tests.js
| }); | ||
|
|
||
| test('field4', ({getModule}) => { | ||
| test('basic class analysis', ({getModule}) => { |
There was a problem hiding this comment.
Moved all the class-specific stuff to classes_test.js, and just left a basic class analysis test here.
| * Note, the `docNode` may differ from the `declaration` in | ||
| * the case of a const assignment to a class expression, as | ||
| * the docs will be on the VariableStatement rather than | ||
| * the class-like expression. |
There was a problem hiding this comment.
There's a getJSDocTags function in the TS repo where the docs claim it gets related JSDoc tags even if they're on other nodes. Would that work here?
There was a problem hiding this comment.
parseNodeJSDocInfo uses that here:
But it doesn't seem to return the e.g. @description, etc. tag on the parent variable statement when the node passed to it is the class expression initializer.
Oddly it does do the thing you expected for const functions, just not for const classes. I had made the equivalent API's between classes and functions line up (i.e. take an optional docNode), but seems like it's not strictly required for functions for whatever reason. I guess I'll yank that back out of the function side...
| statement: ts.VariableStatement, | ||
| dec: ts.VariableDeclaration | ts.EnumDeclaration, |
There was a problem hiding this comment.
I think statement can be retrieved from a VariableDeclaration with .parent.parent. Also, what is statement's relationship to dec when dec is an EnumDeclaration?
There was a problem hiding this comment.
Oh, I just noticed there's a getEnumDeclaration and getEnumDeclarationInfo below. Does getVariableDeclaration need to handle both VariableDeclarations and EnumDeclarations?
There was a problem hiding this comment.
Ah, the EnumDeclaration in that union was vestigial; prior to this getDeclarationInfo handled both variables and enums, but after introducing the statement arg, I had to split them out. Removed.
There was a problem hiding this comment.
I think statement can be retrieved from a VariableDeclaration with .parent.parent.
Oh yeah good point. Hmm, it'd need to be cast though, since VariableDeclaration can exist in for/for of/for in statements also. Feels a little stricter as-is?
There was a problem hiding this comment.
What about checking if .parent.parent is a VariableStatement?
Adds support for analyzing const variables initialized to class or function expressions as ClassDeclaration and FunctionDeclaration, respectively:
Since we didn't previously have a
classestest suite itself (the class analysis was mostly covered by a combination of the LitElement basic and jsdoc tests) and I needed to add some more clasess tests, I pulled the class-specific tests out oflit-element/jsdoc_test.andvanilla-element/jsdoc_test.jsand moved them to a newjavascript/classes_tests.js, and added the newconsttests there. Now, the jsdoc test just contains CE-specific information that's only pulled out of jsdoc (e.g. slot, custom props, etc.). I think this makes the test organization more logical.