UNPKG

@eclipse-scout/migrate

Version:

TypeScript migration module

667 lines (622 loc) 21.1 kB
/* * Copyright (c) 2010, 2023 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ /* eslint-disable indent */ import path from 'path'; export function lfToCrlf(text) { return text.replace(/(?!\r)\n/gm, '\r\n'); } export function crlfToLf(text) { return text.replace(/\r\n/gm, '\n'); } export function inConstructor(path) { return !!findParentPath(path, parentPath => parentPath.node.type === 'ClassMethod' && parentPath.node.kind === 'constructor'); } export function findParentClassBody(path) { return findParentPath(path, parentPath => parentPath.node.type === 'ClassBody'); } export function findClassName(path) { return findParentPath(path, parentPath => parentPath.node.type === 'ClassDeclaration').value.id.name; } export function findParentPath(path, predicate) { let cur = path; while (cur.node.type !== 'Program') { if (predicate(cur)) { return cur; } cur = cur.parentPath; } return undefined; } export function findClassProperty(classBody, propertyName) { return classBody.node.body.find( n => n.type === 'ClassProperty' && n.key.type === 'Identifier' && n.key.name === propertyName ); } /** * @typedef {object} TypeDesc * @property type * @property module?: string */ /** * @returns {TypeDesc} */ export function getTypeFor(j, name, value, typeMaps) { switch (value.type) { case 'StringLiteral': return {type: j.tsStringKeyword()}; case 'BooleanLiteral': return {type: j.tsBooleanKeyword()}; case 'NumericLiteral': return {type: j.tsNumberKeyword()}; case 'NewExpression': return {type: j.tsTypeReference(j.identifier(value.callee.name))}; case 'ArrayExpression': { if (value.elements.length === 0) { // If element is empty, find type based on name. return findTypeByName(j, typeMaps, name) || {type: j.tsArrayType(j.tsAnyKeyword())}; } let elementType = getTypeFor(j, null, value.elements[0]).type; return {type: j.tsArrayType(elementType)}; } default: { let typeDesc = findTypeByName(j, typeMaps, name); if (typeDesc) { return typeDesc; } return {type: j.tsAnyKeyword()}; } } } export function getNameForType(j, type) { if (!type) { return null; } switch (type.type) { case 'TSStringKeyword': return 'string'; case 'TSBooleanKeyword': return 'boolean'; case 'TSNumberKeyword': return 'number'; case 'TSAnyKeyword': return 'any'; case 'TSTypeReference': return type.typeName.name; case 'TSArrayType': { return getNameForType(j, type.elementType) + '[]'; } default: { return null; } } } /** * @returns {TypeDesc} the codeshift type for the string based names used in type maps. */ export function mapType(j, name) { if (name.endsWith('[]')) { name = name.substring(0, name.length - 2); let typeDesc = mapType(j, name); if (typeDesc) { return {type: j.tsArrayType(typeDesc.type), module: typeDesc.module}; } } let type; let module; switch (name) { case 'string': type = j.tsStringKeyword(); break; case 'boolean': type = j.tsBooleanKeyword(); break; case 'number': type = j.tsNumberKeyword(); break; case 'any': type = j.tsAnyKeyword(); break; case 'void': type = j.tsVoidKeyword(); break; default: if (name.indexOf('.') > -1) { [module, name] = name.split('.'); } type = j.tsTypeReference(j.identifier(name)); break; } return {type, module}; } /** * @returns {TypeDesc|null} */ export function findTypeByName(j, typeMaps, name) { if (!name) { return null; } let type = _findTypeByName(); if (type) { return type; } // Ignore leading _ and try again if (name.startsWith('_')) { name = name.substring(1, name.length); } return _findTypeByName(); function _findTypeByName() { for (let map of typeMaps) { if (map.predicate(name)) { return mapType(j, map.type); } } } } export function methodFilter(j, path) { return path.node.type === j.ClassMethod.name || // All exported methods in a file that is not a class (e.g. utilities) (!findParentClassBody(path) && path.node?.type === 'FunctionDeclaration' && path.parentPath.node.type === 'ExportNamedDeclaration'); } export function isOneOf(value, ...args) { if (args.length === 0) { return false; } let argsToCheck = args; if (args.length === 1 && Array.isArray(args[0])) { argsToCheck = args[0]; } return argsToCheck.indexOf(value) !== -1; } export function findIndex(arr, predicate) { if (!arr || !predicate) { return -1; } for (let i = 0; i < arr.length; i++) { if (predicate(arr[i], i, arr)) { return i; } } return -1; } export function findLastIndex(arr, predicate) { if (!arr || !predicate) { return -1; } let index = -1; for (let i = 0; i < arr.length; i++) { if (predicate(arr[i], i, arr)) { index = i; } } return index; } /** * @returns {Collection<ImportDeclaration>} */ export function findImportDeclarations(j, source, predicate) { return source .find(j.ImportDeclaration) .filter(path => predicate(path.node.source.value)); } /** * Returns the {@link ImportSpecifier}s for the given specifier name in the import declaration. * E.g. if the specifierName is b and the import declaration `import {a, b} from 'module'`, the {@link ImportSpecifier} for b wil be returned because it is in the list. * @param {Collection<ImportDeclaration>} importDeclaration * @param {string} specifierName * @returns {Collection<ImportSpecifier>} */ export function findImportSpecifiers(j, importDeclaration, specifierName) { return importDeclaration .find(j.ImportSpecifier) .filter(path => path.value.imported.name === specifierName); } /** * @param {Collection<ImportDeclaration>} importDeclaration * @param {string} specifierName */ export function hasImportSpecifier(j, importDeclaration, specifierName) { return !!findImportSpecifiers(j, importDeclaration, specifierName)?.length; } /** * Inserts a new import to a declaration. * E.g. if specifierName is b, it will add b to the list of imports (import {a} from 'module' -> import {a, b} from 'module') * @param {Collection<ImportDeclaration>} importDeclaration * @param {string} specifierName */ export function insertImportSpecifier(j, importDeclaration, specifierName) { const importSpecifier = j.importSpecifier(j.identifier(specifierName)); importDeclaration.forEach(declaration => { j(declaration).replaceWith( j.importDeclaration( sortImportSpecifiers([...declaration.node.specifiers, importSpecifier]), declaration.node.source ) ); }); } function getFirstNode(j, source) { return source.find(j.Program).get('body', 0).node; } function getClassName(j, source) { let declarations = source.find(j.ClassDeclaration); if (declarations.length === 0) { return null; } return declarations.get(0).parentPath.value.id.name; } export function sortImportSpecifiers(specifiers) { return specifiers.sort((s1, s2) => { if (!s1.imported || !s2.imported) { return 0; // one specifier is probably an ImportDefaultSpecifier } return s1.imported.name.localeCompare(s2.imported.name); }); } /** * @param {TypeDesc[]} typeDescriptors * @param moduleMap */ export function insertMissingImportsForTypes(j, source, typeDescriptors, moduleMap, currentFilePath) { let modules = typeDescriptors.map(typeDesc => typeDesc.module); if (modules.length === 0) { return; } // Save the comments attached to the first node const firstNode = getFirstNode(j, source); const {comments} = firstNode; const className = getClassName(j, source); for (let i = 0; i < modules.length; i++) { let module = modules[i]; let moduleName = moduleMap[module]; let predicate = name => name === moduleName; if (typeof moduleName === 'function') { predicate = moduleName; } else if (typeof moduleName === 'string' && moduleName.startsWith('path:')) { // Get relative path from current file to module moduleName = path.relative(path.parse(currentFilePath).dir, path.resolve(moduleName.substring(moduleName.indexOf(':') + 1))); // Remove file ending moduleName = moduleName.substring(0, moduleName.lastIndexOf('.')); // Always use forward slashes moduleName = moduleName.replaceAll('\\', '/'); } let declarations = findImportDeclarations(j, source, predicate); if (declarations.length === 0) { insertImportDeclaration(j, source, moduleName); declarations = findImportDeclarations(j, source, predicate); } let typeDesc = typeDescriptors[i]; let typeName = getTypeName(typeDesc.type); if (typeName !== className && !hasImportSpecifier(j, declarations, typeName)) { insertImportSpecifier(j, declarations, typeName); } } // When the imports are replaced, the comment on the first node (likely the copy right header) is removed -> Attach comment again // See also https://github.com/facebook/jscodeshift/blob/master/recipes/retain-first-comment.md const newFirstNode = getFirstNode(j, source); if (newFirstNode !== firstNode) { firstNode.comments = null; // Delete comment if another import was added before that will receive it newFirstNode.comments = comments; } } export function insertImportDeclaration(j, source, moduleName) { if (typeof moduleName !== 'string') { // Not possible to add an import declaration return; } const declaration = j.importDeclaration( [], j.stringLiteral(moduleName) ); let body = source.get().node.program.body; let index = -1; if (isFileImport(moduleName)) { // Put file imports at the end (after imports form node_modules) index = findLastIndex(body, node => node.type === 'ImportDeclaration'); } else { // Put imports from node_modules before file imports index = findLastIndex(body, node => node.type === 'ImportDeclaration' && !isFileImport(node.source.value)); } if (index >= 0) { body.splice(index + 1, 0, declaration); } else { // Insert it at the top of the file body.unshift(declaration); } } function isFileImport(importName) { return importName.includes('./') || importName.includes('../'); } export function removeEmptyLinesBetweenImports(text) { return text.replaceAll(/\r\n\r\nimport /g, '\r\nimport '); } function getTypeName(type) { if (type.type === 'TSArrayType') { return type.elementType.typeName.name; } return type.typeName.name; } export function transformCommentLinesToJsDoc(j, comments) { if (!comments) { return null; } let commentTexts = []; if (comments[0].type === 'CommentBlock') { if (comments[0].value.startsWith('*')) { // Already js doc return comments; } // Convert regular block to lines first commentTexts = comments[0].value.trim().split('\r\n').map(comment => ' ' + comment.trim()); } else { commentTexts = comments.map(comment => comment.value); } let str = ''; if (commentTexts.length === 1) { str += `*${commentTexts[0]} `; } else { str = '*\r\n'; for (let comment of commentTexts) { str += ` *${comment}\r\n`; } } return [j.commentBlock(str)]; } export const defaultParamTypeMap = { JQuery: { predicate: name => name.startsWith('$') || name.startsWith('_$'), // check for _ explicitly to ensure no other predicate matches (e.g. FormField) type: 'JQuery' }, number: { predicate: name => isOneOf(name, 'width', 'height', 'top', 'bottom', 'right', 'left', 'x', 'y', 'length', 'maximumUploadSize', 'viewRangeSize', 'count', 'selectionStart', 'selectionEnd', 'sortCode', 'dense', 'delay', 'maxContentLines', 'useOnlyInVisibleColumns', 'index') || name.endsWith('Length') || name.endsWith('Width') || name.endsWith('Height') || name.endsWith('WidthInPixel') || name.endsWith('Count') || name.endsWith('Top') || name.endsWith('Left') || name.endsWith('Index') || name.endsWith('ingX') || name.endsWith('ingY') || name.endsWith('Delay'), type: 'number' }, boolean: { predicate: name => isOneOf(name, 'loading', 'loaded', 'toggleAction', 'compact', 'exclusiveExpand', 'active', 'visible', 'enabled', 'checked', 'selected', 'selectable', 'hasText', 'invalidate', 'modal', 'closable', 'resizable', 'movable', 'askIfNeedSave', 'showOnOpen', 'scrollable', 'updateDisplayTextOnModify', 'autoRemove', 'mandatory', 'stackable', 'shrinkable', 'required', 'collapsed', 'collapsible', 'expanded', 'expandable', 'editable', 'preventDoubleClick', 'autoCloseExternalWindow', 'hasDate', 'hasTime', 'focused', 'responsive', 'wrapText', 'tabbable', 'virtual', 'busy', 'trimText', 'browseAutoExpandAll', 'browseLoadIncremental', 'browseHierarchy', 'minimized', 'maximized', 'failed', 'running', 'stopped', 'requestPending', 'pending', 'inputMasked', 'formatLower', 'formatUpper', 'marked', 'overflown', 'multiSelect', 'multiCheck', 'scrollToSelection', 'trackLocation', 'autoFit', 'multiline', 'multilineText', 'hierarchical', 'loadIncremental', 'hidden', 'hiddenByUi', 'shown', 'withArrow', 'trimWidth', 'trimHeight', 'autoResizeColumns', 'filterAccepted', 'withPlaceholders', 'clickable', 'empty', 'changing', 'inheritAccessibility', 'embedDetailContent', 'displayable', 'compacted', 'autoOptimizeWidth') || name.endsWith('Visible') || name.endsWith('Enabled') || name.endsWith('Focused') || name.endsWith('Required') || name.endsWith('Collapsed') || name.endsWith('Minimized') || name.endsWith('Focusable') || name.endsWith('Active') || name.endsWith('Expanded'), type: 'boolean' }, string: { predicate: name => isOneOf(name, 'displayText', 'text', 'cssClass', 'displayViewId', 'title', 'subTitle', 'subtitle', 'titleSuffix', 'iconId', 'label', 'subLabel', 'imageUrl', 'logoUrl', 'titleSuffix') || name.endsWith('IconId') || name.endsWith('CssClass') || name.endsWith('Text'), type: 'string' }, Date: { predicate: name => name.endsWith('Date') || name.endsWith('Time'), type: 'Date' }, HtmlComponent: { predicate: name => isOneOf(name, 'htmlComp', 'htmlContainer', 'htmlBody', 'htmlChild'), type: 'scout.HtmlComponent' }, Session: { predicate: name => name === 'session', type: 'scout.Session' }, Desktop: { predicate: name => name === 'desktop', type: 'scout.Desktop' }, Actions: { predicate: name => isOneOf(name, 'actions'), type: 'scout.Action[]' }, Popup: { predicate: name => isOneOf(name, 'popup'), type: 'scout.Popup' }, Popups: { predicate: name => isOneOf(name, 'popups'), type: 'scout.Popup[]' }, Insets: { predicate: name => isOneOf(name, 'insets'), type: 'scout.Insets' }, IconDesc: { predicate: name => isOneOf(name, 'iconDesc'), type: 'scout.IconDesc' }, Accordion: { predicate: name => isOneOf(name, 'accordion'), type: 'scout.Accordion' }, BreadCrumbItem: { predicate: name => isOneOf(name, 'breadCrumbItem'), type: 'scout.BreadCrumbItem' }, BreadCrumbItems: { predicate: name => isOneOf(name, 'breadCrumbItems'), type: 'scout.BreadCrumbItem[]' }, Menu: { predicate: name => isOneOf(name, 'menu', 'menuItem'), type: 'scout.Menu' }, Menus: { predicate: name => isOneOf(name, 'menus', 'menuItems', 'staticMenus', 'detailMenus', 'nodeMenus'), type: 'scout.Menu[]' }, LookupCall: { predicate: name => isOneOf(name, 'lookupCall'), type: 'scout.LookupCall' }, CodeType: { predicate: name => isOneOf(name, 'codeType'), type: 'scout.CodeType' }, LookupRow: { predicate: name => isOneOf(name, 'lookupRow'), type: 'scout.LookupRow' }, LookupRows: { predicate: name => isOneOf(name, 'lookupRows'), type: 'scout.LookupRow[]' }, LookupResult: { predicate: name => isOneOf(name, 'lookupResult'), type: 'scout.LookupResult' }, FormField: { predicate: name => isOneOf(name, 'formField', 'field') || name.endsWith('Field'), type: 'scout.FormField' }, FormFields: { predicate: name => isOneOf(name, 'formFields', 'fields') || name.endsWith('Fields'), type: 'scout.FormField[]' }, Column: { predicate: name => isOneOf(name, 'column'), type: 'scout.Column' }, Columns: { predicate: name => isOneOf(name, 'columns'), type: 'scout.Column[]' }, GridData: { predicate: name => isOneOf(name, 'gridData', 'gridDataHints'), type: 'scout.GridData' }, Form: { predicate: name => isOneOf(name, 'form', 'displayParent') || name.endsWith('Form'), type: 'scout.Form' }, Status: { predicate: name => isOneOf(name, 'errorStatus'), type: 'scout.Status' }, Outline: { predicate: name => isOneOf(name, 'outline'), type: 'scout.Outline' }, OutlineOverview: { predicate: name => isOneOf(name, 'outlineOverview'), type: 'scout.OutlineOverview' }, Page: { predicate: name => isOneOf(name, 'page'), type: 'scout.Page' }, TabItem: { predicate: name => isOneOf(name, 'tabItem'), type: 'scout.TabItem' }, TabItems: { predicate: name => isOneOf(name, 'tabItems'), type: 'scout.TabItem[]' }, KeyStroke: { predicate: name => isOneOf(name, 'keyStroke'), type: 'scout.KeyStroke' }, Cell: { predicate: name => isOneOf(name, 'cell', 'headerCell'), type: 'scout.Cell' }, Table: { predicate: name => isOneOf(name, 'table', 'detailTable'), type: 'scout.Table' }, TableControl: { predicate: name => isOneOf(name, 'tableControl'), type: 'scout.TableControl' }, TableControls: { predicate: name => isOneOf(name, 'tableControls'), type: 'scout.TableControl[]' }, Tree: { predicate: name => isOneOf(name, 'tree'), type: 'scout.Tree' }, TileGrid: { predicate: name => isOneOf(name, 'tileGrid'), type: 'scout.TileGrid' }, Tile: { predicate: name => isOneOf(name, 'tile', 'focusedTile'), type: 'scout.Tile' }, Tiles: { predicate: name => isOneOf(name, 'tiles'), type: 'scout.Tile[]' }, Range: { predicate: name => isOneOf(name, 'viewRange'), type: 'scout.Range' }, Widget: { predicate: name => isOneOf(name, 'widget', 'displayParent') || name.endsWith('Widget'), type: 'scout.Widget' }, Widgets: { predicate: name => isOneOf(name, 'widgets'), type: 'scout.Widget[]' } }; export const defaultReturnTypeMap = { JQuery: { predicate: name => name.startsWith('$') || name.startsWith('get$'), type: 'JQuery' }, Dimension: { predicate: name => isOneOf(name, 'prefSize', 'preferredLayoutSize'), type: 'scout.Dimension' }, KeyStrokeContext: { predicate: name => name === '_createKeyStrokeContext', type: 'scout.KeyStrokeContext' }, boolean: { predicate: name => isOneOf(name, 'equals') || name.match(/^is[A-Z]/) || name.match(/^has[A-Z]/), type: 'boolean' }, string: { predicate: name => isOneOf(name, 'toString'), type: 'string' } }; // Value may be a function as well export const defaultModuleMap = { scout: '@eclipse-scout/core' }; export const defaultRecastOptions = { quote: 'single', objectCurlySpacing: false }; export const defaultMenuTypesMap = { 'Table.EmptySpace': {objectType: 'Table', menuTypes: 'MenuType', menuType: 'EmptySpace'}, 'Table.SingleSelection': {objectType: 'Table', menuTypes: 'MenuType', menuType: 'SingleSelection'}, 'Table.MultiSelection': {objectType: 'Table', menuTypes: 'MenuType', menuType: 'MultiSelection'}, 'Table.Header': {objectType: 'Table', menuTypes: 'MenuType', menuType: 'Header'}, 'TabBox.Header': {objectType: 'TabBox', menuTypes: 'MenuType', menuType: 'Header'}, 'Tree.EmptySpace': {objectType: 'Tree', menuTypes: 'MenuType', menuType: 'EmptySpace'}, 'Tree.SingleSelection': {objectType: 'Tree', menuTypes: 'MenuType', menuType: 'SingleSelection'}, 'Tree.MultiSelection': {objectType: 'Tree', menuTypes: 'MenuType', menuType: 'MultiSelection'}, 'Tree.Header': {objectType: 'Tree', menuTypes: 'MenuType', menuType: 'Header'}, 'Planner.Activity': {objectType: 'Planner', menuTypes: 'MenuType', menuType: 'Activity'}, 'Planner.EmptySpace': {objectType: 'Planner', menuTypes: 'MenuType', menuType: 'EmptySpace'}, 'Planner.Range': {objectType: 'Planner', menuTypes: 'MenuType', menuType: 'Range'}, 'Planner.Resource': {objectType: 'Planner', menuTypes: 'MenuType', menuType: 'Resource'}, 'Calendar.EmptySpace': {objectType: 'Calendar', menuTypes: 'MenuType', menuType: 'EmptySpace'}, 'Calendar.CalendarComponent': {objectType: 'Calendar', menuTypes: 'MenuType', menuType: 'CalendarComponent'} };