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

feat: support custom expression precedence #427

Merged
Merged
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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ Checkout the [live demo](http://david.bonnet.cc/astring/demo/) showing Astring i
- [Import](#import)
- [API](#api)
- [`generate(node: object, options: object): string | object`](#generatenode-object-options-object-string-%7C-object)
- [`GENERATOR: object`](#generator-object)
- [`EXPRESSIONS_PRECEDENCE: object`](#expressions_precedence-object)
- [`NEEDS_PARENTHESES: number`](#needs_parentheses-number)
- [`baseGenerator: object`](#basegenerator-object)
- [Benchmark](#benchmark)
- [Generating code](#generating-code)
Expand Down Expand Up @@ -100,11 +103,24 @@ The `options` are:
- `output`: output stream to write the rendered code to (defaults to `null`)
- `generator`: custom code generator (defaults to `astring.baseGenerator`)
- `sourceMap`: [source map generator](https://github.com/mozilla/source-map#sourcemapgenerator) (defaults to `null`)
- `expressionsPrecedence`: custom map of node types and their precedence level (defaults to `EXPRESSIONS_PRECEDENCE`)

### `baseGenerator: object`
### `GENERATOR: object`

Base generator that can be used to [extend Astring](#extending).

### `EXPRESSIONS_PRECEDENCE: object`

Mapping of node types and their precedence level to let the generator know when to use parentheses.

### `NEEDS_PARENTHESES: number`

Default precedence level that always triggers the use of parentheses.

### `baseGenerator: object`

> :warning: Deprecated, use `GENERATOR` instead.

## Benchmark

### Generating code
Expand Down
55 changes: 34 additions & 21 deletions src/astring.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ const OPERATOR_PRECEDENCE = {
}

// Enables parenthesis regardless of precedence
const NEEDS_PARENTHESES = 17
export const NEEDS_PARENTHESES = 17

const EXPRESSIONS_PRECEDENCE = {
export const EXPRESSIONS_PRECEDENCE = {
// Definitions
ArrayExpression: 20,
TaggedTemplateExpression: 20,
Expand Down Expand Up @@ -106,12 +106,12 @@ function formatSequence(state, nodes) {
state.write(')')
}

function expressionNeedsParenthesis(node, parentNode, isRightHand) {
const nodePrecedence = EXPRESSIONS_PRECEDENCE[node.type]
function expressionNeedsParenthesis(state, node, parentNode, isRightHand) {
const nodePrecedence = state.expressionsPrecedence[node.type]
if (nodePrecedence === NEEDS_PARENTHESES) {
return true
}
const parentNodePrecedence = EXPRESSIONS_PRECEDENCE[parentNode.type]
const parentNodePrecedence = state.expressionsPrecedence[parentNode.type]
if (nodePrecedence !== parentNodePrecedence) {
// Different node types
return (
Expand Down Expand Up @@ -150,7 +150,7 @@ function formatBinaryExpressionPart(state, node, parentNode, isRightHand) {
The `isRightHand` parameter should be `true` if the `node` is a right-hand argument.
*/
const { generator } = state
if (expressionNeedsParenthesis(node, parentNode, isRightHand)) {
if (expressionNeedsParenthesis(state, node, parentNode, isRightHand)) {
state.write('(')
generator[node.type](node, state)
state.write(')')
Expand Down Expand Up @@ -240,7 +240,10 @@ let ForInStatement,
ArrayExpression,
BlockStatement

export const baseGenerator = {
export const GENERATOR = {
/*
Default generator.
*/
Program(node, state) {
const indent = state.indent.repeat(state.indentLevel)
const { lineEnd, writeComments } = state
Expand Down Expand Up @@ -302,7 +305,7 @@ export const baseGenerator = {
state.write(';')
},
ExpressionStatement(node, state) {
const precedence = EXPRESSIONS_PRECEDENCE[node.expression.type]
const precedence = state.expressionsPrecedence[node.expression.type]
if (
precedence === NEEDS_PARENTHESES ||
(precedence === 3 && node.expression.left.type[0] === 'O')
Expand Down Expand Up @@ -565,7 +568,7 @@ export const baseGenerator = {
state.write('export default ')
this[node.declaration.type](node.declaration, state)
if (
EXPRESSIONS_PRECEDENCE[node.declaration.type] &&
state.expressionsPrecedence[node.declaration.type] &&
node.declaration.type[0] !== 'F'
) {
// All expression nodes except `FunctionExpression`
Expand Down Expand Up @@ -837,8 +840,8 @@ export const baseGenerator = {
state.write(' ')
}
if (
EXPRESSIONS_PRECEDENCE[node.argument.type] <
EXPRESSIONS_PRECEDENCE.UnaryExpression
state.expressionsPrecedence[node.argument.type] <
state.expressionsPrecedence.UnaryExpression
) {
state.write('(')
this[node.argument.type](node.argument, state)
Expand Down Expand Up @@ -888,8 +891,8 @@ export const baseGenerator = {
LogicalExpression: BinaryExpression,
ConditionalExpression(node, state) {
if (
EXPRESSIONS_PRECEDENCE[node.test.type] >
EXPRESSIONS_PRECEDENCE.ConditionalExpression
state.expressionsPrecedence[node.test.type] >
state.expressionsPrecedence.ConditionalExpression
) {
this[node.test.type](node.test, state)
} else {
Expand All @@ -905,8 +908,8 @@ export const baseGenerator = {
NewExpression(node, state) {
state.write('new ')
if (
EXPRESSIONS_PRECEDENCE[node.callee.type] <
EXPRESSIONS_PRECEDENCE.CallExpression ||
state.expressionsPrecedence[node.callee.type] <
state.expressionsPrecedence.CallExpression ||
hasCallExpression(node.callee)
) {
state.write('(')
Expand All @@ -919,8 +922,8 @@ export const baseGenerator = {
},
CallExpression(node, state) {
if (
EXPRESSIONS_PRECEDENCE[node.callee.type] <
EXPRESSIONS_PRECEDENCE.CallExpression
state.expressionsPrecedence[node.callee.type] <
state.expressionsPrecedence.CallExpression
) {
state.write('(')
this[node.callee.type](node.callee, state)
Expand All @@ -938,8 +941,8 @@ export const baseGenerator = {
},
MemberExpression(node, state) {
if (
EXPRESSIONS_PRECEDENCE[node.object.type] <
EXPRESSIONS_PRECEDENCE.MemberExpression
state.expressionsPrecedence[node.object.type] <
state.expressionsPrecedence.MemberExpression
) {
state.write('(')
this[node.object.type](node.object, state)
Expand Down Expand Up @@ -989,6 +992,11 @@ export const baseGenerator = {

const EMPTY_OBJECT = {}

/*
DEPRECATED: Alternate export of `GENERATOR`.
*/
export const baseGenerator = GENERATOR

class State {
constructor(options) {
const setup = options == null ? EMPTY_OBJECT : options
Expand All @@ -1000,7 +1008,11 @@ class State {
} else {
this.output = ''
}
this.generator = setup.generator != null ? setup.generator : baseGenerator
this.generator = setup.generator != null ? setup.generator : GENERATOR
this.expressionsPrecedence =
setup.expressionsPrecedence != null
? setup.expressionsPrecedence
: EXPRESSIONS_PRECEDENCE
// Formating setup
this.indent = setup.indent != null ? setup.indent : ' '
this.lineEnd = setup.lineEnd != null ? setup.lineEnd : '\n'
Expand Down Expand Up @@ -1110,7 +1122,8 @@ export function generate(node, options) {
- `startingIndentLevel`: indent level to start from (defaults to `0`)
- `comments`: generate comments if `true` (defaults to `false`)
- `output`: output stream to write the rendered code to (defaults to `null`)
- `generator`: custom code generator (defaults to `baseGenerator`)
- `generator`: custom code generator (defaults to `GENERATOR`)
- `expressionsPrecedence`: custom map of node types and their precedence level (defaults to `EXPRESSIONS_PRECEDENCE`)
*/
const state = new State(options)
// Travel through the AST node and generate the code
Expand Down