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 all 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 function declaration without a name'
);
}
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),
};
};

const getFunctionDeclaration = (
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": []
}