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

Added getTypesFromComment() function. #24

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,428 changes: 4,428 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

40 changes: 22 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"scripts": {
"build": "tsc",
"cli": "npm run build && node dist/cli.js",
"test": "mocha --require ts-node/register --timeout 60000 --watch-extensions ts \"**/*.spec.ts\"",
"test": "mocha --require ts-node/register --timeout 70000 --watch-extensions ts \"**/*.spec.ts\"",
"prepublishOnly": "npm test && npm run build"
},
"keywords": [
Expand All @@ -29,25 +29,29 @@
"dist/"
],
"dependencies": {
"argparse": "^1.0.10",
"fast-glob": "^3.2.4",
"graphlib": "^2.1.5",
"lodash": "^4.17.20",
"resolve": "^1.8.1",
"trace-error": "^0.0.7",
"ts-morph": "^8.1.0",
"typescript": "^4.0.0",
"winston": "^3.0.0"
"argparse": "^2.0.1",
"fast-glob": "^3.2.7",
"graphlib": "^2.1.8",
"jsdoc-parse": "^6.0.1",
"jsdoc-to-markdown": "^7.1.0",
"lodash": "^4.17.21",
"os": "^0.1.2",
"resolve": "^1.20.0",
"trace-error": "^1.0.3",
"ts-morph": "^12.2.0",
"typescript": "^4.5.2",
"winston": "^3.3.3"
},
"devDependencies": {
"@types/chai": "^4.1.4",
"@types/graphlib": "^2.1.4",
"@types/lodash": "^4.14.112",
"@types/mocha": "^5.2.4",
"@types/node": "^10.5.2",
"@types/chai": "^4.2.22",
"@types/graphlib": "^2.1.8",
"@types/jsdoc-to-markdown": "^7.0.1",
"@types/lodash": "^4.14.177",
"@types/mocha": "^9.0.0",
"@types/node": "^16.11.9",
"@types/winston": "^2.3.9",
"chai": "^4.1.2",
"mocha": "^5.2.0",
"ts-node": "^7.0.0"
"chai": "^4.3.4",
"mocha": "^9.1.3",
"ts-node": "^10.4.0"
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Project, ClassInstancePropertyTypes, PropertyDeclarationStructure, Scope } from "ts-morph";
import { Project, ClassInstancePropertyTypes, ClassDeclaration, PropertyDeclarationStructure, Scope, JSDocParameterTag, JSDoc, JSDocTag, JSDocUnknownTag, JSDocPropertyTag } from "ts-morph";
import { parseJsClasses } from "./parse-js-classes";
import { correctJsProperties } from "./correct-js-properties";
import logger from "../../logger/logger";
import { jsDocElement } from "../jsDocElement";

/**
* Parses all source files looking for ES6 classes, and takes any `this`
Expand All @@ -25,55 +26,237 @@ import logger from "../../logger/logger";
* }
* }
*/
export function addClassPropertyDeclarations( tsAstProject: Project ): Project {
export function addClassPropertyDeclarations(tsAstProject: Project): Project {
// Parse the JS classes for all of the this.xyz properties that they use
const jsClasses = parseJsClasses( tsAstProject );
const jsClasses = parseJsClasses(tsAstProject);

// Correct the JS classes' properties for superclass/subclass relationships
// (essentially remove properties from subclasses that are defined by their
// superclasses)
const propertiesCorrectedJsClasses = correctJsProperties( jsClasses );
const propertiesCorrectedJsClasses = correctJsProperties(jsClasses);

// Fill in field definitions for each of the classes
propertiesCorrectedJsClasses.forEach( jsClass => {
const sourceFile = tsAstProject.getSourceFileOrThrow( jsClass.path );
logger.verbose( ` Updating class '${jsClass.name}' in '${sourceFile.getFilePath()}'` );
propertiesCorrectedJsClasses.forEach((jsClass) => {
const sourceFile = tsAstProject.getSourceFileOrThrow(jsClass.path);
logger.verbose(` Updating class '${jsClass.name}' in '${sourceFile.getFilePath()}'`);

const classDeclaration = sourceFile.getClassOrThrow( jsClass.name! );
const classDeclaration = sourceFile.getClassOrThrow(jsClass.name!);
const jsClassProperties = jsClass.properties;
const jsDocElements = getJsDocElements(classDeclaration);

// If the utility was run against a TypeScript codebase, we should not
// fill in property declarations for properties that are already
// declared in the class. However, we *should* fill in any missing
// declarations. Removing any already-declared declarations from the
// jsClassProperties.
const currentPropertyDeclarations = classDeclaration.getInstanceProperties()
.reduce( ( props: Set<string>, prop: ClassInstancePropertyTypes ) => {
const propName = prop.getName();
return propName ? props.add( propName ) : props;
}, new Set<string>() );
const currentPropertyDeclarations = classDeclaration.getInstanceProperties().reduce((props: Set<string>, prop: ClassInstancePropertyTypes) => {
const propName = prop.getName();
return propName ? props.add(propName) : props;
}, new Set<string>());

let undeclaredProperties = [ ...jsClassProperties ]
.filter( ( propName: string ) => !currentPropertyDeclarations.has( propName ) );
let undeclaredProperties = [...jsClassProperties].filter((propName: string) => !currentPropertyDeclarations.has(propName));

// If the utility found a reference to this.constructor, we don't want to
// add a property called 'constructor'. Filter that out now.
// https://github.com/gregjacobs/js-to-ts-converter/issues/9
undeclaredProperties = undeclaredProperties
.filter( ( propName: string ) => propName !== 'constructor' );
undeclaredProperties = undeclaredProperties.filter((propName: string) => propName !== "constructor");

// Add all currently-undeclared properties
const propertyDeclarations = undeclaredProperties.map( propertyName => {
const propertyDeclarations = undeclaredProperties.map((propertyName, i: number) => {
const types = getTypes(propertyName, classDeclaration, jsDocElements);

// If optional add '?' to property name in undeclaredProperties
if (types?.tsIsOptional) {
undeclaredProperties[i] = types?.tsName!;
}
return {
name: propertyName,
type: 'any',
scope: Scope.Public
name: types?.tsIsOptional ? types?.tsName + "?" : types?.tsName,
type: types?.tsType,
scope: types?.tsIsPrivate ? Scope.Private : Scope.Public,
} as PropertyDeclarationStructure;
} );
});

if (undeclaredProperties && undeclaredProperties.length > 0) {
logger.verbose(` Adding property declarations for properties: '${undeclaredProperties.join("', '")}'`);
}

classDeclaration.insertProperties(0, propertyDeclarations);

logger.verbose( ` Adding property declarations for properties: '${undeclaredProperties.join( "', '" )}'` );
classDeclaration.insertProperties( 0, propertyDeclarations );
} );
// logger.verbose(` Setting property defaults for properties: '${undeclaredProperties.join("', '")}'`);
undeclaredProperties.map((propertyName) => {
const types = getTypes(propertyName, classDeclaration, jsDocElements);

// Add default value to a property
const propDeclaration = classDeclaration.getPropertyOrThrow(propertyName);
if (propDeclaration !== null) {
if (types?.tsDefault) {
propDeclaration.setInitializer(types?.tsDefault);
}
if (types?.commentText !== "") {
const comments = propDeclaration.getLeadingCommentRanges();
}
}
});
});

return tsAstProject;
}
}

function getTypesFromComment(
propertyName: string,
classDeclaration: ClassDeclaration
): {
tsName: string;
tsType: string;
tsIsPrivate: boolean;
tsIsOptional: boolean;
tsIsUnion: boolean;
tsDefault: string;
commentText: string;
oaType: string;
oaFormat: string;
} {
let tsName = propertyName;
let tsType = "any";
let tsIsPrivate = false;
let tsIsOptional = false;
let tsIsUnion = false;
let tsDefault = "";
let commentText = "";
let oaType = "";
let oaFormat = "";
const constructors = classDeclaration.getConstructors().map((constructor) => {
const statements = constructor.getStatementsWithComments().map((statement) => {
const statementText = statement.getText();
if (statementText.includes(propertyName)) {
const comments = statement.getTrailingCommentRanges().map((comment) => {
commentText = comment.getText();

// Types: [`TS Type` #TS Default# ^OA Type^ ~OA Format~] - https://regex101.com

// TS Type pattern: (?<=`).*(?=`)
const tsTypeMatch = commentText.match(/(?<=`).*(?=`)/);
if (tsTypeMatch !== null) {
tsType = tsTypeMatch[0];
}

// TS is Optional pattern: (?<=@).*(?=@)
const tsIsOptionalMatch = commentText.match(/(?<=@).*(?=@)/);
if (tsIsOptionalMatch !== null) {
tsIsOptional = tsIsOptionalMatch[0] === "true";
}

// TS Default pattern: (?<=#).*(?=#)
const tsDefaultMatch = commentText.match(/(?<=#).*(?=#)/);
if (tsDefaultMatch !== null) {
tsDefault = tsDefaultMatch[0];
}

// OA Type pattern: (?<=\^).*(?=\^)
const oaTypeMatch = commentText.match(/(?<=\^).*(ß?=\^)/);
if (oaTypeMatch !== null) {
oaType = oaTypeMatch[0];
}

// OA Format pattern: (?<=~).*(?=~)
const oaFormatMatch = commentText.match(/(?<=~).*(?=~)/);
if (oaFormatMatch !== null) {
oaFormat = oaFormatMatch[0];
}
});
}
});
});
return { tsName, tsType, tsIsPrivate, tsIsOptional, tsIsUnion, tsDefault, commentText, oaType, oaFormat };
}

function getJsDocElements(classDecl: ClassDeclaration | undefined): jsDocElement[] | undefined {
const jsDocElements: jsDocElement[] = [];

for (let i = 0; i <= 1; i++) {
let jsdocs: JSDoc[] | undefined;

// Get class JSDoc
if (i === 0) {
jsdocs = classDecl?.getJsDocs();
}

// Get Constructor JSDoc
if (i === 1) {
const constrDecl = classDecl?.getConstructors()[0];
jsdocs = constrDecl?.getJsDocs();
}

jsdocs?.forEach((jsDoc: JSDoc, i: number) => {
const tags = jsDoc.getTags();
const element = new jsDocElement();

element.description = jsDoc.getDescription();
element.className = classDecl?.getName();

if (i === 1) {
element.methodName = "constructor";
}

tags?.forEach((tag: JSDocTag) => {
const tagElement = new jsDocElement();
tagElement.isTag = true;
tagElement.tagName = tag.getTagName();
tagElement.tagcomment = tag.getCommentText();
tagElement.tagText = tag.getText();

if (tag instanceof JSDocUnknownTag && tagElement.tagName == "property") {
tagElement.setParamNameAndType(tagElement.tagcomment);
} else if (tag instanceof JSDocParameterTag || tag instanceof JSDocPropertyTag) {
const paramTag = tag as JSDocParameterTag;

tagElement.isParamBracketed = paramTag.isBracketed();
tagElement.paramName = paramTag.getName();
tagElement.paramType = paramTag.getTypeExpression()?.getTypeNode()?.getText();
}
if (tagElement.isParam) {
jsDocElements.push(tagElement);
}
});
jsDocElements.push(element);
});
}
return jsDocElements.length > 0 ? jsDocElements : undefined;
}

function getTypes(
propertyName: string,
classDeclaration: ClassDeclaration,
jsDocElements: jsDocElement[] | undefined
): {
tsName: string | undefined;
tsType: string | undefined;
tsIsPrivate: boolean | undefined;
tsIsOptional: boolean | undefined;
tsIsUnion: boolean | undefined;
tsDefault: string;
commentText: string | undefined;
oaType: string | undefined;
oaFormat: string | undefined;
} | null {
if (jsDocElements && jsDocElements?.length > 0) {
const jsDocElement = getPropertyType(propertyName, jsDocElements);
let tsName = jsDocElement?.paramName;
let tsType = jsDocElement?.paramType;
let tsIsPrivate = jsDocElement?.isParamPrivate;
let tsIsOptional = jsDocElement?.isParamTypeOptional;
let tsIsUnion = jsDocElement?.isParamTypeUnion;
let tsDefault = "";
let commentText = jsDocElement?.tagcomment;
let oaType = undefined;
let oaFormat = undefined;

return { tsName, tsType, tsIsPrivate, tsIsOptional, tsIsUnion, tsDefault, commentText, oaType, oaFormat };
} else {
return getTypesFromComment(propertyName, classDeclaration);
}
}

function getPropertyType(propertyName: string, jsDocElements: jsDocElement[]): jsDocElement | undefined {
return jsDocElements.find((item) => item.paramName === propertyName);
}
Loading