Skip to content

Commit

Permalink
feat: improve syntax error output and top-level await/return parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Aug 17, 2022
1 parent e0c1d9d commit 4328f66
Show file tree
Hide file tree
Showing 18 changed files with 447 additions and 144 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"@jedwards1211/commitlint-config": "^1.0.2",
"@jedwards1211/eslint-config": "^2.0.2",
"@jedwards1211/eslint-config-typescript": "^2.0.2",
"@types/babel__code-frame": "^7.0.3",
"@types/babel__generator": "^7.6.3",
"@types/babel__traverse": "^7.14.2",
"@types/chai": "^4.2.14",
Expand Down Expand Up @@ -160,12 +161,13 @@
"typescript": "^4.1.3"
},
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/generator": "^7.15.0",
"@babel/runtime": "^7.12.5",
"@babel/traverse": "^7.15.0",
"@babel/types": "^7.15.0",
"ast-types": "^0.14.2",
"babel-parse-wild-code": "^1.1.1",
"babel-parse-wild-code": "^1.2.0",
"chalk": "^4.1.0",
"debug": "^4.3.1",
"dedent-js": "^1.0.1",
Expand Down
20 changes: 19 additions & 1 deletion src/cli/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import isEmpty from 'lodash/isEmpty'
import inquirer from 'inquirer'
import fs from 'fs-extra'
import dedent from 'dedent-js'
import CodeFrameError from '../jscodeshift/util/CodeFrameError'
import { codeFrameColumns } from '@babel/code-frame'

/* eslint-disable no-console */

Expand Down Expand Up @@ -142,7 +144,23 @@ const transform: CommandModule<Options> = {
if (error) {
errorCount++
logHeader(console.error)
console.error(chalk.red(error.stack))
if (error instanceof CodeFrameError && error.source && error.loc) {
console.error(
dedent`
${chalk.red(
`Error in ${error.filename} (${error.loc.start.line}:${error.loc.start.column})`
)}
${codeFrameColumns(error.source, error.loc, {
highlightCode: true,
forceColor: true,
message: error.message,
})}
${chalk.red(error.stack?.replace(/^.*?(\r\n?|\n)/, ''))}
`
)
} else {
console.error(chalk.red(error.stack))
}
} else if (source && transformed && source !== transformed) {
changedCount++
results[file] = transformed
Expand Down
222 changes: 127 additions & 95 deletions src/jscodeshift/Astx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import find, { Match, convertWithCaptures, createMatch } from './find'
import replace from './replace'
import parseFindOrReplace from './util/parseFindOrReplace'
import compileMatcher, { MatchResult } from './compileMatcher'
import CodeFrameError from './util/CodeFrameError'

export type ParseTag = (
strings: TemplateStringsArray,
Expand Down Expand Up @@ -204,54 +205,65 @@ export default class Astx {
| TemplateStringsArray,
...rest: any[]
): Astx | ((options?: FindOptions) => Astx) {
let paths: ASTPath<any>[], options: FindOptions | undefined
if (typeof arg0 === 'string') {
paths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, [arg0] as any) as
| ASTNode
| ASTNode[]
).paths()
options = rest[0]
} else if (isNode(arg0) || isNodeArray(arg0)) {
paths = this.jscodeshift(arg0).paths()
options = rest[0]
} else if (isNodePath(arg0)) {
paths = [arg0]
options = rest[0]
} else if (isNodePathArray(arg0)) {
paths = arg0
options = rest[0]
} else {
const finalPaths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, arg0 as any, ...rest) as
| ASTNode
| ASTNode[]
).paths()
return (options?: FindOptions) => this.closest(finalPaths, options) as any
}
if (paths.length !== 1) {
throw new Error(`must be a single node`)
}
const matcher = compileMatcher(paths[0], options)
const matchSoFar = this._createInitialMatch()
try {
let paths: ASTPath<any>[], options: FindOptions | undefined
if (typeof arg0 === 'string') {
paths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, [arg0] as any) as
| ASTNode
| ASTNode[]
).paths()
options = rest[0]
} else if (isNode(arg0) || isNodeArray(arg0)) {
paths = this.jscodeshift(arg0).paths()
options = rest[0]
} else if (isNodePath(arg0)) {
paths = [arg0]
options = rest[0]
} else if (isNodePathArray(arg0)) {
paths = arg0
options = rest[0]
} else {
const finalPaths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, arg0 as any, ...rest) as
| ASTNode
| ASTNode[]
).paths()
return (options?: FindOptions) =>
this.closest(finalPaths, options) as any
}
if (paths.length !== 1) {
throw new Error(`must be a single node`)
}
const matcher = compileMatcher(paths[0], options)
const matchSoFar = this._createInitialMatch()

const matchedParents: Set<ASTPath> = new Set()
const matches: Match[] = []
this._paths.forEach((path) => {
let parent = path.parent
while (parent) {
if (matchedParents.has(parent)) return
const match = matcher.match(parent, matchSoFar)
if (match) {
matchedParents.add(parent)
matches.push(createMatch(parent, match))
return
const matchedParents: Set<ASTPath> = new Set()
const matches: Match[] = []
this._paths.forEach((path) => {
let parent = path.parent
while (parent) {
if (matchedParents.has(parent)) return
const match = matcher.match(parent, matchSoFar)
if (match) {
matchedParents.add(parent)
matches.push(createMatch(parent, match))
return
}
parent = parent.parent
}
parent = parent.parent
}
})
})

return new Astx(this.jscodeshift, matches)
return new Astx(this.jscodeshift, matches)
} catch (error) {
if (error instanceof Error) {
CodeFrameError.rethrow(error, {
filename: 'find pattern',
source: typeof arg0 === 'string' ? arg0 : undefined,
})
}
throw error
}
}

find(
Expand All @@ -272,35 +284,45 @@ export default class Astx {
| TemplateStringsArray,
...rest: any[]
): Astx | ((options?: FindOptions) => Astx) {
let pattern, options: FindOptions | undefined
if (typeof arg0 === 'string') {
pattern = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, [arg0] as any) as
| ASTNode
| ASTNode[]
).paths()
options = rest[0]
} else if (isNode(arg0) || isNodeArray(arg0)) {
pattern = this.jscodeshift(arg0).paths()
options = rest[0]
} else if (isNodePath(arg0) || isNodePathArray(arg0)) {
pattern = arg0
options = rest[0]
} else {
const finalPaths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, arg0 as any, ...rest) as
| ASTNode
| ASTNode[]
).paths()
return (options?: FindOptions) => this.find(finalPaths, options) as any
try {
let pattern, options: FindOptions | undefined
if (typeof arg0 === 'string') {
pattern = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, [arg0] as any) as
| ASTNode
| ASTNode[]
).paths()
options = rest[0]
} else if (isNode(arg0) || isNodeArray(arg0)) {
pattern = this.jscodeshift(arg0).paths()
options = rest[0]
} else if (isNodePath(arg0) || isNodePathArray(arg0)) {
pattern = arg0
options = rest[0]
} else {
const finalPaths = this.jscodeshift(
parseFindOrReplace(this.jscodeshift, arg0 as any, ...rest) as
| ASTNode
| ASTNode[]
).paths()
return (options?: FindOptions) => this.find(finalPaths, options) as any
}
return new Astx(
this.jscodeshift,
find(this._paths, pattern, {
...options,
matchSoFar: this._createInitialMatch(),
})
)
} catch (error) {
if (error instanceof Error) {
CodeFrameError.rethrow(error, {
filename: 'find pattern',
source: typeof arg0 === 'string' ? arg0 : undefined,
})
}
throw error
}
return new Astx(
this.jscodeshift,
find(this._paths, pattern, {
...options,
matchSoFar: this._createInitialMatch(),
})
)
}

replace(strings: TemplateStringsArray, ...quasis: any[]): () => void
Expand All @@ -309,28 +331,38 @@ export default class Astx {
arg0: string | ASTNode | ASTNode[] | GetReplacement | TemplateStringsArray,
...quasis: any[]
): void | (() => void) {
const { _matches, _parseTag, jscodeshift } = this
if (typeof arg0 === 'function') {
replace(
_matches,
(match: Match): ASTNode => {
const result = arg0(match, _parseTag)
return typeof result === 'string'
? (parseFindOrReplace(jscodeshift, [result] as any) as any)
: result
}
)
} else if (typeof arg0 === 'string') {
replace(_matches, parseFindOrReplace(jscodeshift, [arg0] as any) as any)
} else if (isNode(arg0) || isNodeArray(arg0)) {
replace(_matches, arg0 as any)
} else {
const parsed = parseFindOrReplace(
jscodeshift,
arg0 as any,
...quasis
) as any
return () => replace(_matches, parsed)
try {
const { _matches, _parseTag, jscodeshift } = this
if (typeof arg0 === 'function') {
replace(
_matches,
(match: Match): ASTNode => {
const result = arg0(match, _parseTag)
return typeof result === 'string'
? (parseFindOrReplace(jscodeshift, [result] as any) as any)
: result
}
)
} else if (typeof arg0 === 'string') {
replace(_matches, parseFindOrReplace(jscodeshift, [arg0] as any) as any)
} else if (isNode(arg0) || isNodeArray(arg0)) {
replace(_matches, arg0 as any)
} else {
const parsed = parseFindOrReplace(
jscodeshift,
arg0 as any,
...quasis
) as any
return () => replace(_matches, parsed)
}
} catch (error) {
if (error instanceof Error) {
CodeFrameError.rethrow(error, {
filename: 'replace pattern',
source: typeof arg0 === 'string' ? arg0 : undefined,
})
}
throw error
}
}
}
1 change: 1 addition & 0 deletions src/jscodeshift/compileMatcher/CallExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function compileCallExpressionMatcher(

if (callee.type === 'Identifier') {
const special = compileSpecialMatcher(
path,
callee.name,
path.get('arguments').filter(() => true),
compileOptions
Expand Down
1 change: 1 addition & 0 deletions src/jscodeshift/compileMatcher/GenericTypeAnnotation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function compileGenericTypeAnnotationMatcher(
if (captureMatcher) return captureMatcher
} else {
const special = compileSpecialMatcher(
path,
id.name,
path
.get('typeParameters')
Expand Down
17 changes: 14 additions & 3 deletions src/jscodeshift/compileMatcher/SpecialMatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,37 @@ import { CompiledMatcher, CompileOptions } from '.'
import compileOptionalMatcher from './Optional'
import compileOrMatcher from './Or'
import compileAndMatcher from './And'
import CompilePathError from '../util/CompilePathError'

export default function compileSpecialMatcher(
path: ASTPath,
name: string,
params: ASTPath[],
compileOptions: CompileOptions
): CompiledMatcher | void {
switch (name) {
case '$Optional':
if (params.length !== 1) {
throw new Error(`$Optional must be used with 1 type parameter`)
throw new CompilePathError(
`$Optional must be used with 1 type parameter`,
path
)
}
return compileOptionalMatcher(params[0], compileOptions)
case '$Or':
if (params.length < 2) {
throw new Error(`$Or must be called with at least 2 arguments`)
throw new CompilePathError(
`$Or must be called with at least 2 arguments`,
path
)
}
return compileOrMatcher(params, compileOptions)
case '$And':
if (params.length < 2) {
throw new Error(`$And must be called with at least 2 arguments`)
throw new CompilePathError(
`$And must be called with at least 2 arguments`,
path
)
}
return compileAndMatcher(params, compileOptions)
}
Expand Down
1 change: 1 addition & 0 deletions src/jscodeshift/compileMatcher/TSTypeReference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function compileTSTypeReferenceMatcher(
if (captureMatcher) return captureMatcher
} else {
const special = compileSpecialMatcher(
path,
typeName.name,
path
.get('typeParameters')
Expand Down
3 changes: 3 additions & 0 deletions src/jscodeshift/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ export {
Transform,
TransformResult,
}

export { default as CodeFrameError } from './util/CodeFrameError'
export { default as CompilePathError } from './util/CompilePathError'
Loading

0 comments on commit 4328f66

Please sign in to comment.