UNPKG

@rightcapital/php-parser

Version:

TypeScript types for PHP Parser JSON representation

292 lines (256 loc) • 8.2 kB
#!/usr/bin/env ts-node import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs'; import * as os from 'node:os'; import { resolve } from 'node:path'; import * as path from 'node:path'; import * as _ from 'lodash'; import * as Mustache from 'mustache'; import { PROJECT_ROOT, SRC_ROOT } from '../constants'; import { CliHelpers } from './helpers/cli-helpers'; import { FilePathHelpers } from './helpers/file-path-helpers'; import { type IProperty, type IUses, NodeRetrieverHelpers, } from './helpers/node-retriever-helpers'; import { TypeGenerationHelpers } from './helpers/type-generation-helpers'; const PHP_PARSER_ROOT_PATH = resolve( PROJECT_ROOT, 'vendor/nikic/php-parser/lib/PhpParser', ); const PHP_PARSER_NODE_DIRECTORY_PATH = resolve(PHP_PARSER_ROOT_PATH, 'Node'); export interface INodeOutput { nodeName: string; fullyQualifiedNodeName: string; fullyQualifiedParentNodeName: string; parentNodeParts: string[]; parentNodeName: string; parentNodeFilePath: string; properties: IProperty[]; nodeType: string; fileName: string; filePath: string; filePathParts: string[]; uses: IUses; uniqueImportDeclarationGeneratedStrings: string[]; } export interface IContextNodeItem { name: string; nodeType: string; subNodeNames: string[]; parentNodeName: string; filePath: string; properties: IProperty[]; } export interface ICliContext { allNodeNames: string[]; allNodes: { [nodeType: string]: IContextNodeItem; }; temporaryNodeTypeRootPath: string; } class GenerateType { /** * This context is a global context available on whole cli running process */ private static cliContext: ICliContext = { allNodeNames: [], allNodes: {}, temporaryNodeTypeRootPath: resolve(os.tmpdir(), 'node'), }; public static main(): void { const typesRootPath = FilePathHelpers.getTypesRootPath(); const nodeTypeRootPath = resolve(typesRootPath, 'node'); // Remove and create a node root path first mkdirSync(GenerateType.cliContext.temporaryNodeTypeRootPath, { recursive: true, }); GenerateType.walkSync( PHP_PARSER_NODE_DIRECTORY_PATH, // eslint-disable-next-line @typescript-eslint/unbound-method GenerateType.generateTypeForNodeByFile, ); const typesFileName = resolve(typesRootPath, 'types.ts'); const temporaryTypesFileName = resolve(typesRootPath, 'tmp-types.ts'); writeFileSync( temporaryTypesFileName, TypeGenerationHelpers.generateCombinationTypesFromNodes( GenerateType.cliContext, ), { encoding: 'utf8' }, ); // Move temporarily generated files to correct path // We need to remove the outdated files first rmSync(nodeTypeRootPath, { recursive: true, force: true }); rmSync(typesFileName, { force: true }); // Move them renameSync( GenerateType.cliContext.temporaryNodeTypeRootPath, nodeTypeRootPath, ); renameSync(temporaryTypesFileName, typesFileName); } private static generateTypeForNodeByFile(filePath: string): void { const fileRelativePath = path.relative( PHP_PARSER_NODE_DIRECTORY_PATH, filePath, ); const filePathParts: string[] = filePath.split('/'); const directoryRelativePath = fileRelativePath .split('/') .slice(0, -1) .map((part) => _.kebabCase(part)) .join('/'); const fileName = _.kebabCase( fileRelativePath .split('/') .pop() .replace(/\.php$/, ''), ); const ast = CliHelpers.parsePhpFileToAst(filePath); const classNode = NodeRetrieverHelpers.getRootClassNode(ast); const uses = NodeRetrieverHelpers.getUsesMap(ast); if (classNode) { const properties = NodeRetrieverHelpers.getPropertiesFromClassNode( classNode, filePathParts, fileRelativePath, uses, ); const uniqueImportDeclarationGeneratedStrings = [ ...new Set( properties .map( (property) => property.typeGenerationPackage .importDeclarationGeneratedStrings, ) .flat(), ), ]; const parentNodeFilePath = FilePathHelpers.getFilePathFromNameNodeParts( NodeRetrieverHelpers.getPartsByName(classNode.extends.name), classNode.extends.nodeType, filePathParts, uses, ); const fullyQualifiedNodeName = FilePathHelpers.getFullyQualifiedNodeNameByFilePath(fileRelativePath); const parentNodeName = NodeRetrieverHelpers.getPartsByName( classNode.extends.name, ).at(-1); const fullyQualifiedParentNodeName = FilePathHelpers.getFullyQualifiedParentNodeNameByFilePath( fileRelativePath, parentNodeFilePath, parentNodeName, ); const nodeOutput: INodeOutput = { fileName, filePath: path.join(directoryRelativePath, fileName), filePathParts, fullyQualifiedNodeName, fullyQualifiedParentNodeName, nodeName: classNode.name.name, parentNodeParts: NodeRetrieverHelpers.getPartsByName( classNode.extends.name, ), parentNodeName, parentNodeFilePath, properties, uniqueImportDeclarationGeneratedStrings, nodeType: NodeRetrieverHelpers.getGetTypeFunctionReturnValue(classNode), uses, }; GenerateType.appendNodeToContext(nodeOutput); const templateFileName = path.resolve( SRC_ROOT, 'templates/php-parser-node.mustache', ); const templateFileContent = readFileSync(templateFileName, 'utf8'); try { if (directoryRelativePath) { mkdirSync( path.resolve( GenerateType.cliContext.temporaryNodeTypeRootPath, directoryRelativePath, ), { recursive: true, }, ); } } finally { writeFileSync( resolve( GenerateType.cliContext.temporaryNodeTypeRootPath, directoryRelativePath, `${fileName}.ts`, ), Mustache.render(templateFileContent, nodeOutput), ); } } } private static walkSync( directoryPath: string, callback: typeof GenerateType.generateTypeForNodeByFile, ): void { const fileNames = readdirSync(directoryPath); fileNames.forEach((fileName) => { const filePath = path.join(directoryPath, fileName); const stats = statSync(filePath); if (stats.isDirectory()) { GenerateType.walkSync(filePath, callback); } else if (stats.isFile()) { callback(filePath); } }); } private static appendNodeToContext(node: INodeOutput) { GenerateType.cliContext.allNodeNames.push(node.nodeName); if (!(node.fullyQualifiedNodeName in GenerateType.cliContext.allNodes)) { GenerateType.cliContext.allNodes[node.fullyQualifiedNodeName] = { name: node.fullyQualifiedNodeName, subNodeNames: [], parentNodeName: '', filePath: '', properties: [], nodeType: '', }; } if ( !(node.fullyQualifiedParentNodeName in GenerateType.cliContext.allNodes) ) { GenerateType.cliContext.allNodes[node.fullyQualifiedParentNodeName] = { name: node.fullyQualifiedParentNodeName, subNodeNames: [], parentNodeName: '', filePath: '', properties: [], nodeType: '', }; } GenerateType.cliContext.allNodes[ node.fullyQualifiedParentNodeName ].subNodeNames.push(node.fullyQualifiedNodeName); GenerateType.cliContext.allNodes[ node.fullyQualifiedNodeName ].parentNodeName = node.fullyQualifiedParentNodeName; GenerateType.cliContext.allNodes[node.fullyQualifiedNodeName].filePath = node.filePath; GenerateType.cliContext.allNodes[node.fullyQualifiedNodeName].properties = node.properties; GenerateType.cliContext.allNodes[node.fullyQualifiedNodeName].nodeType = node.nodeType; } } GenerateType.main();