Skip to content
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

[labs/analyzer] Add support for analyzing function declarations #3655

Merged
merged 5 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nice-rockets-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': minor
---

Added support for analyzing function declarations.
1 change: 1 addition & 0 deletions packages/labs/analyzer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type {
PackageJson,
ModuleWithLitElementDeclarations,
DeprecatableDescribed,
FunctionDeclaration,
} from './lib/model.js';

export type {AbsolutePath, PackagePath} from './lib/paths.js';
Expand Down
53 changes: 51 additions & 2 deletions packages/labs/analyzer/src/lib/javascript/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,58 @@

import ts from 'typescript';
import {DiagnosticsError} from '../errors.js';
import {AnalyzerInterface, Parameter, Return} from '../model.js';
import {
AnalyzerInterface,
DeclarationInfo,
FunctionDeclaration,
Parameter,
Return,
} from '../model.js';
import {getTypeForNode, getTypeForType} from '../types.js';
import {parseJSDocDescription} from './jsdoc.js';
import {parseJSDocDescription, parseNodeJSDocInfo} from './jsdoc.js';
import {hasDefaultModifier, hasExportModifier} from '../utils.js';

/**
* Returns the name of a function declaration.
*/
const getFunctionDeclarationName = (declaration: ts.FunctionDeclaration) => {
const name =
declaration.name?.text ??
// The only time a function declaration will not have a name is when it is
// a default export, aka `export default function() {...}`
(hasDefaultModifier(declaration) ? 'default' : undefined);
if (name === undefined) {
throw new DiagnosticsError(
declaration,
'Unexpected class declaration without a name'
kevinpschaaf marked this conversation as resolved.
Show resolved Hide resolved
);
}
return name;
};

export const getFunctionDeclarationInfo = (
declaration: ts.FunctionDeclaration,
analyzer: AnalyzerInterface
): DeclarationInfo => {
const name = getFunctionDeclarationName(declaration);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the other stuff in getFunctionLikeInfo() too expensive to call here? This feels like something that should be rolled into it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getFunctionLikeInfo() is called here and in ClassMethod

return {
name,
factory: () => getFunctionDeclaration(declaration, name, analyzer),
isExport: hasExportModifier(declaration),
};
};

export const getFunctionDeclaration = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const getFunctionDeclaration = (
const getFunctionDeclaration = (

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

declaration: ts.FunctionLikeDeclaration,
name: string,
analyzer: AnalyzerInterface
): FunctionDeclaration => {
return new FunctionDeclaration({
name,
...parseNodeJSDocInfo(declaration),
...getFunctionLikeInfo(declaration, analyzer),
});
};

/**
* Returns information on FunctionLike nodes
Expand Down
3 changes: 3 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
getSpecifierString,
} from '../references.js';
import {parseModuleJSDocInfo} from './jsdoc.js';
import {getFunctionDeclarationInfo} from './functions.js';

/**
* Returns the sourcePath, jsPath, and package.json contents of the containing
Expand Down Expand Up @@ -116,6 +117,8 @@ export const getModule = (
for (const statement of sourceFile.statements) {
if (ts.isClassDeclaration(statement)) {
addDeclaration(getClassDeclarationInfo(statement, analyzer));
} else if (ts.isFunctionDeclaration(statement)) {
addDeclaration(getFunctionDeclarationInfo(statement, analyzer));
} else if (ts.isVariableStatement(statement)) {
getVariableDeclarationInfo(statement, analyzer).forEach(addDeclaration);
} else if (ts.isEnumDeclaration(statement)) {
Expand Down
6 changes: 1 addition & 5 deletions packages/labs/analyzer/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,21 +355,17 @@ export interface ClassMethodInit extends FunctionLikeInit {
source?: SourceReference | undefined;
}

export class ClassMethod extends Declaration {
export class ClassMethod extends FunctionDeclaration {
static?: boolean | undefined;
privacy?: Privacy | undefined;
inheritedFrom?: Reference | undefined;
source?: SourceReference | undefined;
parameters?: Parameter[] | undefined;
return?: Return | undefined;
constructor(init: ClassMethodInit) {
super(init);
this.static = init.static;
this.privacy = init.privacy;
this.inheritedFrom = init.inheritedFrom;
this.source = init.source;
this.parameters = init.parameters;
this.return = init.return;
}
}

Expand Down
115 changes: 115 additions & 0 deletions packages/labs/analyzer/src/test/javascript/functions_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import {suite} from 'uvu';
// eslint-disable-next-line import/extensions
import * as assert from 'uvu/assert';
import {fileURLToPath} from 'url';
import {getSourceFilename, languages} from '../utils.js';

import {
createPackageAnalyzer,
Analyzer,
AbsolutePath,
Module,
} from '../../index.js';

for (const lang of languages) {
const test = suite<{
analyzer: Analyzer;
packagePath: AbsolutePath;
module: Module;
}>(`Module tests (${lang})`);

test.before((ctx) => {
try {
const packagePath = fileURLToPath(
new URL(`../../test-files/${lang}/functions`, import.meta.url).href
) as AbsolutePath;
const analyzer = createPackageAnalyzer(packagePath);

const result = analyzer.getPackage();
const file = getSourceFilename('functions', lang);
const module = result.modules.find((m) => m.sourcePath === file);
if (module === undefined) {
throw new Error(`Analyzer did not analyze file '${file}'`);
}

ctx.packagePath = packagePath;
ctx.analyzer = analyzer;
ctx.module = module;
} catch (error) {
// Uvu has a bug where it silently ignores failures in before and after,
// see https://github.com/lukeed/uvu/issues/191.
console.error('uvu before error', error);
process.exit(1);
}
Comment on lines +28 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also a chunk like this in ./modules_test.ts. Probably not worth making a function yet if it's just the two, but are there others?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will separate into follow-up PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});

test('Function 1', ({module}) => {
const exportedFn = module.getResolvedExport('function1');
const fn = module.getDeclaration('function1');
assert.equal(fn, exportedFn);
assert.ok(fn?.isFunctionDeclaration());
assert.equal(fn.name, `function1`);
assert.equal(fn.description, `Function 1 description`);
assert.equal(fn.summary, undefined);
assert.equal(fn.parameters?.length, 0);
assert.equal(fn.deprecated, undefined);
});

test('Function 2', ({module}) => {
const exportedFn = module.getResolvedExport('function2');
const fn = module.getDeclaration('function2');
assert.equal(fn, exportedFn);
assert.ok(fn?.isFunctionDeclaration());
assert.equal(fn.name, `function2`);
assert.equal(fn.summary, `Function 2 summary\nwith wraparound`);
assert.equal(fn.description, `Function 2 description\nwith wraparound`);
assert.equal(fn.parameters?.length, 3);
assert.equal(fn.parameters?.[0].name, 'a');
assert.equal(fn.parameters?.[0].description, 'Param a description');
assert.equal(fn.parameters?.[0].summary, undefined);
assert.equal(fn.parameters?.[0].type?.text, 'string');
assert.equal(fn.parameters?.[0].default, undefined);
assert.equal(fn.parameters?.[0].rest, false);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uvu's equal() is a deep equal... can you use that for better readability?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to do pretty bad things with classes, so gonna avoid for now.

assert.equal(fn.parameters?.[1].name, 'b');
assert.equal(
fn.parameters?.[1].description,
'Param b description\nwith wraparound'
);
assert.equal(fn.parameters?.[1].type?.text, 'boolean');
assert.equal(fn.parameters?.[1].optional, true);
assert.equal(fn.parameters?.[1].default, 'false');
assert.equal(fn.parameters?.[1].rest, false);
assert.equal(fn.parameters?.[2].name, 'c');
assert.equal(fn.parameters?.[2].description, 'Param c description');
assert.equal(fn.parameters?.[2].summary, undefined);
assert.equal(fn.parameters?.[2].type?.text, 'number[]');
assert.equal(fn.parameters?.[2].optional, false);
assert.equal(fn.parameters?.[2].default, undefined);
assert.equal(fn.parameters?.[2].rest, true);
assert.equal(fn.return?.type?.text, 'string');
assert.equal(fn.return?.description, 'Function 2 return description');
assert.equal(fn.deprecated, 'Function 2 deprecated');
});

test('Default function', ({module}) => {
const exportedFn = module.getResolvedExport('default');
const fn = module.getDeclaration('default');
assert.equal(fn, exportedFn);
assert.ok(fn?.isFunctionDeclaration());
assert.equal(fn.name, `default`);
assert.equal(fn.description, `Default function description`);
assert.equal(fn.summary, undefined);
assert.equal(fn.parameters?.length, 0);
assert.equal(fn.return?.type?.text, 'string');
assert.equal(fn.return?.description, 'Default function return description');
assert.equal(fn.deprecated, undefined);
});

test.run();
}
38 changes: 38 additions & 0 deletions packages/labs/analyzer/test-files/js/functions/functions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* Function 1 description
*/
export function function1() {}

/**
* @summary Function 2 summary
* with wraparound
*
* @description Function 2 description
* with wraparound
*
* @param {string} a Param a description
* @param {boolean} b Param b description
* with wraparound
*
* @param {number[]} c Param c description
* @returns {string} Function 2 return description
*
* @deprecated Function 2 deprecated
*/
export function function2(a, b = false, ...c) {
return b ? a : c[0].toFixed();
}

/**
* Default function description
* @returns {string} Default function return description
*/
export default function () {
return 'default';
}
6 changes: 6 additions & 0 deletions packages/labs/analyzer/test-files/js/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@lit-internal/test-functions",
"dependencies": {
"lit": "^2.0.0"
}
}
6 changes: 6 additions & 0 deletions packages/labs/analyzer/test-files/ts/functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "@lit-internal/test-functions",
"dependencies": {
"lit": "^2.0.0"
}
}
38 changes: 38 additions & 0 deletions packages/labs/analyzer/test-files/ts/functions/src/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* Function 1 description
*/
export function function1() {}

/**
* @summary Function 2 summary
* with wraparound
*
* @description Function 2 description
* with wraparound
*
* @param a Param a description
* @param b Param b description
* with wraparound
*
* @param c Param c description
* @returns Function 2 return description
*
* @deprecated Function 2 deprecated
*/
export function function2(a: string, b = false, ...c: number[]) {
return b ? a : c[0].toFixed();
}

/**
* Default function description
* @returns Default function return description
*/
export default function () {
return 'default';
}
16 changes: 16 additions & 0 deletions packages/labs/analyzer/test-files/ts/functions/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es2020",
"lib": ["es2020", "DOM"],
"module": "ES2020",
"rootDir": "./src",
"outDir": "./",
"moduleResolution": "node",
"experimentalDecorators": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"noEmit": true
},
"include": ["src/**/*.ts"],
"exclude": []
}