diff --git a/.cspell.json b/.cspell.json index b3e10009914..39e8c795e7a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,7 @@ "**/**/CHANGELOG.md", "**/**/CONTRIBUTORS.md", "**/**/ROADMAP.md", - "**/*.json" + "**/*.{json,snap}" ], "dictionaries": [ "typescript", diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index c20b6a39025..1a465e160aa 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -55,12 +55,10 @@ "@types/debug": "^4.1.5", "@types/glob": "^7.1.1", "@types/is-glob": "^4.0.1", - "@types/lodash.isplainobject": "^4.0.4", "@types/lodash.unescape": "^4.0.4", "@types/semver": "^6.2.0", "@types/tmp": "^0.1.0", "@typescript-eslint/shared-fixtures": "2.14.0", - "lodash.isplainobject": "4.0.6", "tmp": "^0.1.0", "typescript": "*" }, diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 82755ef9907..fb62c090bb7 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -366,7 +366,7 @@ function parseAndGenerateServices( )!; /** - * Determine whatever or not two-way maps of converted AST nodes should be preserved + * Determine if two-way maps of converted AST nodes should be preserved * during the conversion process */ const shouldPreserveNodeMaps = diff --git a/packages/typescript-estree/tests/ast-alignment/parse.ts b/packages/typescript-estree/tests/ast-alignment/parse.ts index a56efd778f6..42f6e0f68fc 100644 --- a/packages/typescript-estree/tests/ast-alignment/parse.ts +++ b/packages/typescript-estree/tests/ast-alignment/parse.ts @@ -3,7 +3,6 @@ import { ParserPlugin } from '@babel/parser'; import { codeFrameColumns } from '@babel/code-frame'; import * as parser from '../../src/parser'; -import * as parseUtils from './utils'; function createError( message: string, @@ -92,14 +91,10 @@ export function parse( try { switch (opts.parser) { case '@typescript-eslint/typescript-estree': - result.ast = parseUtils.normalizeNodeTypes( - parseWithTypeScriptESTree(text, opts.jsx), - ); + result.ast = parseWithTypeScriptESTree(text, opts.jsx); break; case '@babel/parser': - result.ast = parseUtils.normalizeNodeTypes( - parseWithBabelParser(text, opts.jsx), - ); + result.ast = parseWithBabelParser(text, opts.jsx); break; default: throw new Error( diff --git a/packages/typescript-estree/tests/ast-alignment/spec.ts b/packages/typescript-estree/tests/ast-alignment/spec.ts index 4c16a4143c8..9e308b73c64 100644 --- a/packages/typescript-estree/tests/ast-alignment/spec.ts +++ b/packages/typescript-estree/tests/ast-alignment/spec.ts @@ -78,7 +78,7 @@ fixturesToTest.forEach(fixture => { ), ).toEqual( parseUtils.removeLocationDataAndSourceTypeFromProgramNode( - typeScriptESTreeResult.ast, + parseUtils.preprocessTypescriptAST(typeScriptESTreeResult.ast), fixture.ignoreSourceType, ), ); diff --git a/packages/typescript-estree/tests/ast-alignment/utils.ts b/packages/typescript-estree/tests/ast-alignment/utils.ts index 78b7df66fe6..4830a7f1f5a 100644 --- a/packages/typescript-estree/tests/ast-alignment/utils.ts +++ b/packages/typescript-estree/tests/ast-alignment/utils.ts @@ -1,90 +1,25 @@ // babel types are something we don't really care about /* eslint-disable @typescript-eslint/no-explicit-any */ -import { AST_NODE_TYPES } from '../../src/ts-estree'; -import isPlainObject from 'lodash.isplainobject'; - -/** - * By default, pretty-format (within Jest matchers) retains the names/types of nodes from the babylon AST, - * quick and dirty way to avoid that is to JSON.stringify and then JSON.parser the - * ASTs before comparing them with pretty-format - * - * @param {Object} ast raw AST - * @returns {Object} normalized AST - */ -export function normalizeNodeTypes(ast: any): any { - return JSON.parse(JSON.stringify(ast)); -} - -/** - * Removes the given keys from the given AST object recursively - * @param root A JavaScript object to remove keys from - * @param keysToOmit Names and predicate functions use to determine what keys to omit from the final object - * @param nodes advance ast modifications - * @returns {Object} formatted object - */ -export function omitDeep( - root: any, - keysToOmit: { key: string; predicate: Function }[], - nodes: Record void> = {}, -): any { - function shouldOmit(keyName: string, val: any): boolean { - if (keysToOmit?.length) { - return keysToOmit.some( - keyConfig => keyConfig.key === keyName && keyConfig.predicate(val), - ); - } - return false; - } - - function visit(node: any, parent: any): void { - if (!node) { - return; - } - - for (const prop in node) { - if (Object.prototype.hasOwnProperty.call(node, prop)) { - if (shouldOmit(prop, node[prop])) { - delete node[prop]; - continue; - } - - const child = node[prop]; - - if (Array.isArray(child)) { - for (const el of child) { - visit(el, node); - } - } else if (isPlainObject(child)) { - visit(child, node); - } - } - } - - if (typeof node.type === 'string' && node.type in nodes) { - nodes[node.type](node, parent); - } - } - - visit(root, null); - return root; -} +import { AST_NODE_TYPES, TSESTree } from '../../src/ts-estree'; +import { deeplyCopy, omitDeep } from '../../tools/test-utils'; +import * as BabelTypes from '@babel/types'; /** * Common predicates for Babylon AST preprocessing */ const always = (): boolean => true; -const ifNumber = (val: any): boolean => typeof val === 'number'; +const ifNumber = (val: unknown): boolean => typeof val === 'number'; /** * - Babylon wraps the "Program" node in an extra "File" node, normalize this for simplicity for now... * - Remove "start" and "end" values from Babylon nodes to reduce unimportant noise in diffs ("loc" data will still be in * each final AST and compared). * - * @param {Object} ast raw babylon AST - * @returns {Object} processed babylon AST + * @param ast raw babylon AST + * @returns processed babylon AST */ -export function preprocessBabylonAST(ast: any): any { - return omitDeep( +export function preprocessBabylonAST(ast: BabelTypes.File): any { + return omitDeep( ast.program, [ { @@ -130,7 +65,7 @@ export function preprocessBabylonAST(ast: any): any { /** * Awaiting feedback on Babel issue https://github.com/babel/babel/issues/9231 */ - TSCallSignatureDeclaration(node: any) { + TSCallSignatureDeclaration(node) { if (node.typeAnnotation) { node.returnType = node.typeAnnotation; delete node.typeAnnotation; @@ -143,7 +78,7 @@ export function preprocessBabylonAST(ast: any): any { /** * Awaiting feedback on Babel issue https://github.com/babel/babel/issues/9231 */ - TSConstructSignatureDeclaration(node: any) { + TSConstructSignatureDeclaration(node) { if (node.typeAnnotation) { node.returnType = node.typeAnnotation; delete node.typeAnnotation; @@ -156,7 +91,7 @@ export function preprocessBabylonAST(ast: any): any { /** * Awaiting feedback on Babel issue https://github.com/babel/babel/issues/9231 */ - TSFunctionType(node: any) { + TSFunctionType(node) { if (node.typeAnnotation) { node.returnType = node.typeAnnotation; delete node.typeAnnotation; @@ -169,7 +104,7 @@ export function preprocessBabylonAST(ast: any): any { /** * Awaiting feedback on Babel issue https://github.com/babel/babel/issues/9231 */ - TSConstructorType(node: any) { + TSConstructorType(node) { if (node.typeAnnotation) { node.returnType = node.typeAnnotation; delete node.typeAnnotation; @@ -182,7 +117,7 @@ export function preprocessBabylonAST(ast: any): any { /** * Awaiting feedback on Babel issue https://github.com/babel/babel/issues/9231 */ - TSMethodSignature(node: any) { + TSMethodSignature(node) { if (node.typeAnnotation) { node.returnType = node.typeAnnotation; delete node.typeAnnotation; @@ -215,7 +150,7 @@ export function preprocessBabylonAST(ast: any): any { }; } }, - ClassProperty(node: any) { + ClassProperty(node) { /** * Babel: ClassProperty + abstract: true * ts-estree: TSAbstractClassProperty @@ -233,7 +168,7 @@ export function preprocessBabylonAST(ast: any): any { node.declare = false; } }, - TSExpressionWithTypeArguments(node: any, parent: any) { + TSExpressionWithTypeArguments(node, parent: any) { if (parent.type === 'TSInterfaceDeclaration') { node.type = 'TSInterfaceHeritage'; } else if ( @@ -266,17 +201,17 @@ export function preprocessBabylonAST(ast: any): any { * babel: sets optional property as true/undefined * ts-estree: sets optional property as true/false */ - MemberExpression(node: any) { + MemberExpression(node) { if (!node.optional) { node.optional = false; } }, - CallExpression(node: any) { + CallExpression(node) { if (!node.optional) { node.optional = false; } }, - OptionalCallExpression(node: any) { + OptionalCallExpression(node) { if (!node.optional) { node.optional = false; } @@ -286,7 +221,7 @@ export function preprocessBabylonAST(ast: any): any { * babel: sets asserts property as true/undefined * ts-estree: sets asserts property as true/false */ - TSTypePredicate(node: any) { + TSTypePredicate(node) { if (!node.asserts) { node.asserts = false; } @@ -302,9 +237,9 @@ export function preprocessBabylonAST(ast: any): any { * * See: https://github.com/babel/babel/issues/6681 * - * @param {Object} ast the raw AST with a Program node at its top level - * @param {boolean} ignoreSourceType fix for issues with unambiguous type detection - * @returns {Object} the ast with the location data removed from the Program node + * @param ast the raw AST with a Program node at its top level + * @param ignoreSourceType fix for issues with unambiguous type detection + * @returns the ast with the location data removed from the Program node */ export function removeLocationDataAndSourceTypeFromProgramNode( ast: any, @@ -317,3 +252,12 @@ export function removeLocationDataAndSourceTypeFromProgramNode( } return ast; } + +/** + * Returns a raw copy of the typescript AST + * @param ast the AST object + * @returns copy of the AST object + */ +export function preprocessTypescriptAST(ast: T): T { + return deeplyCopy(ast); +} diff --git a/packages/typescript-estree/tests/lib/__snapshots__/javascript.ts.snap b/packages/typescript-estree/tests/lib/__snapshots__/javascript.ts.snap index a73f908a362..84f3780a21c 100644 --- a/packages/typescript-estree/tests/lib/__snapshots__/javascript.ts.snap +++ b/packages/typescript-estree/tests/lib/__snapshots__/javascript.ts.snap @@ -132662,7 +132662,7 @@ Object { "pattern": "foo.", }, "type": "Literal", - "value": Object {}, + "value": /foo\\./, }, "loc": Object { "end": Object { @@ -132859,7 +132859,7 @@ Object { "pattern": "[\\\\u{0000000000000061}-\\\\u{7A}]", }, "type": "Literal", - "value": Object {}, + "value": /\\[\\\\u\\{0000000000000061\\}-\\\\u\\{7A\\}\\]/u, }, "loc": Object { "end": Object { @@ -133253,7 +133253,7 @@ Object { "pattern": "foo", }, "type": "Literal", - "value": Object {}, + "value": /foo/u, }, "loc": Object { "end": Object { @@ -133450,7 +133450,7 @@ Object { "pattern": "foo", }, "type": "Literal", - "value": Object {}, + "value": /foo/y, }, "loc": Object { "end": Object { diff --git a/packages/typescript-estree/tools/test-utils.ts b/packages/typescript-estree/tools/test-utils.ts index b9fcb51cab6..2a9b2f11ddf 100644 --- a/packages/typescript-estree/tools/test-utils.ts +++ b/packages/typescript-estree/tools/test-utils.ts @@ -1,22 +1,6 @@ import * as parser from '../src/parser'; import { TSESTreeOptions } from '../src/parser-options'; -/** - * Returns a raw copy of the given AST - * @param {Object} ast the AST object - * @returns {Object} copy of the AST object - */ -export function getRaw(ast: parser.TSESTree.Program): parser.TSESTree.Program { - return JSON.parse( - JSON.stringify(ast, (key, value) => { - if ((key === 'start' || key === 'end') && typeof value === 'number') { - return undefined; - } - return value; - }), - ); -} - export function parseCodeAndGenerateServices( code: string, config: TSESTreeOptions, @@ -27,24 +11,24 @@ export function parseCodeAndGenerateServices( /** * Returns a function which can be used as the callback of a Jest test() block, * and which performs an assertion on the snapshot for the given code and config. - * @param {string} code The source code to parse - * @param {TSESTreeOptions} config the parser configuration - * @param {boolean} generateServices Flag determining whether to generate ast maps and program or not - * @returns {jest.ProvidesCallback} callback for Jest it() block + * @param code The source code to parse + * @param config the parser configuration + * @param generateServices Flag determining whether to generate ast maps and program or not + * @returns callback for Jest it() block */ export function createSnapshotTestBlock( code: string, config: TSESTreeOptions, generateServices?: true, -): () => void { +): jest.ProvidesCallback { /** - * @returns {Object} the AST object + * @returns the AST object */ function parse(): parser.TSESTree.Program { const ast = generateServices ? parser.parseAndGenerateServices(code, config).ast : parser.parse(code, config); - return getRaw(ast); + return deeplyCopy(ast); } return (): void => { @@ -84,3 +68,84 @@ export function isJSXFileType(fileType: string): boolean { } return fileType === 'js' || fileType === 'jsx' || fileType === 'tsx'; } + +/** + * Returns a raw copy of the typescript AST + * @param ast the AST object + * @returns copy of the AST object + */ +export function deeplyCopy(ast: T): T { + return omitDeep(ast) as T; +} + +type UnknownObject = Record; + +function isObjectLike(value: unknown | null): value is UnknownObject { + return ( + typeof value === 'object' && !(value instanceof RegExp) && value !== null + ); +} + +/** + * Removes the given keys from the given AST object recursively + * @param root A JavaScript object to remove keys from + * @param keysToOmit Names and predicate functions use to determine what keys to omit from the final object + * @param selectors advance ast modifications + * @returns formatted object + */ +export function omitDeep( + root: T, + keysToOmit: { key: string; predicate: (value: unknown) => boolean }[] = [], + selectors: Record< + string, + (node: UnknownObject, parent: UnknownObject | null) => void + > = {}, +): UnknownObject { + function shouldOmit(keyName: string, val: unknown): boolean { + if (keysToOmit?.length) { + return keysToOmit.some( + keyConfig => keyConfig.key === keyName && keyConfig.predicate(val), + ); + } + return false; + } + + function visit( + oNode: UnknownObject, + parent: UnknownObject | null, + ): UnknownObject { + if (!Array.isArray(oNode) && !isObjectLike(oNode)) { + return oNode; + } + + const node = { ...oNode }; + + for (const prop in node) { + if (Object.prototype.hasOwnProperty.call(node, prop)) { + if (shouldOmit(prop, node[prop]) || typeof node[prop] === 'undefined') { + delete node[prop]; + continue; + } + + const child = node[prop]; + if (Array.isArray(child)) { + const value = []; + for (const el of child) { + value.push(visit(el, node)); + } + node[prop] = value; + } else if (isObjectLike(child)) { + node[prop] = visit(child, node); + } + } + } + + if (typeof node.type === 'string' && node.type in selectors) { + selectors[node.type](node, parent); + } + + return node; + } + + return visit(root as UnknownObject, null); +} diff --git a/yarn.lock b/yarn.lock index 8ce1cf0d491..56703239ab2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1390,13 +1390,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== -"@types/lodash.isplainobject@^4.0.4": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@types/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#757d2dcdecbb32f4452018b285a586776092efd1" - integrity sha512-8G41YFhmOl8Ck6NrwLK5hhnbz6ADfuDJP+zusDnX3PoYhfC60+H/rQE6zmdO4yFzPCPJPY4oGZK2spbXm6gYEA== - dependencies: - "@types/lodash" "*" - "@types/lodash.memoize@^4.1.4": version "4.1.6" resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.6.tgz#3221f981790a415cab1a239f25c17efd8b604c23" @@ -5487,11 +5480,6 @@ lodash.ismatch@^4.4.0: resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= -lodash.isplainobject@4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= - lodash.map@^4.5.1: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3"