Skip to content

Commit

Permalink
[labs/analyzer] Add support for analyzing function declarations (#3655)
Browse files Browse the repository at this point in the history
* Add support for analyzing function declarations

* Add manifest emit for FunctionDeclaration

* Fix typo

* Remove unused export
  • Loading branch information
kevinpschaaf committed Feb 10, 2023
1 parent b7b01c0 commit 7e20a52
Show file tree
Hide file tree
Showing 14 changed files with 385 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/nice-rockets-smile.md
@@ -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
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
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);
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
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
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
@@ -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);
}
});

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);
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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": []
}

0 comments on commit 7e20a52

Please sign in to comment.