Skip to content

Commit

Permalink
Merge 358e8b7 into 3848ef3
Browse files Browse the repository at this point in the history
  • Loading branch information
simonseyock committed Jan 9, 2022
2 parents 3848ef3 + 358e8b7 commit bf17935
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 56 deletions.
119 changes: 65 additions & 54 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@

# Jsdoc Type Pratt Parser

This project is a parser for jsdoc types. It is heavily inspired by the existing libraries catharsis and
jsdoctypeparser, but does not use PEG.js, instead it is written as a pratt parser.
* https://github.com/hegemonic/catharsis
* https://github.com/jsdoctypeparser/jsdoctypeparser
This project is a parser for jsdoc types. It takes jsdoc type expressions like `Array<string>` and creates an abstract
syntax tree (AST) out of it. It is heavily inspired by the existing libraries [catharsis](https://github.com/hegemonic/catharsis) and [jsdoctypeparser](https://github.com/jsdoctypeparser/jsdoctypeparser), but does
not use [PEG.js](https://pegjs.org/), instead it is written as a pratt parser.

You can find some more information about pratt parsers here:
* https://en.wikipedia.org/wiki/Operator-precedence_parser#Pratt_parsing
* http://journal.stuffwithstuff.com/2011/03/19/pratt-parsers-expression-parsing-made-easy/
* https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html

## Table of Contents
* [Live Demo](#live-demo)
* [Getting Started](#getting-started)
* [API Documentation](#api-documentation)
* [Available Grammars](#available-grammars)
* [Transforms](#transforms)
* [Traverse](#traverse)
* [Tests Status](#tests-status)
* [Performance](#performance)
* [Development](#development)
Expand All @@ -35,62 +39,81 @@ npm install jsdoc-type-pratt-parser@alpha
```js
import { parse } from 'jsdoc-type-pratt-parser'

const result = parse('myType.<string>', 'closure')
const result = parse('SomeType<string>', 'typescript')
```

## API Documentation

An API documentation can be found here: https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html
An API documentation can be found [here](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html).
It is still lacking in some points. Feel free to create issues or PRs to improve this.

## Available Grammars

Three different modes (grammars) are supported: `'jsdoc'`, `'closure'` and `'typescript'`

## Transforms

This library supports compatibility modes for catharsis and jsdoctypeparser. The provided transform functions attempt to
transform the output to the expected output of the target library. This will not always be the same as some types are
parsed differently. These modes are thought to make transition easier, but it is advised to use the native output as
this will be more uniform and will contain more information.

Catharsis compat mode:
A common task to do on ASTs are transforms, for example a stringification. This library includes some transform and
utilities to implement your own.

[`stringify`](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#stringify):

```js
import { parse, catharsisTransform } from 'jsdoc-type-pratt-parser'
import { stringify } from 'jsdoc-type-pratt-parser'

const result = catharsisTransform(parse('myType.<string>', 'closure'))
const val = stringify({ type: 'JsdocTypeName', value: 'name'}) // -> 'name'
```

Jsdoctypeparser compat mode:
You can customize the stringification by using [`stringifyRules`](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#stringifyRules)
and [`transform`](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#transform):

```js
import { parse, jtpTransform } from 'jsdoc-type-pratt-parser'
import { stringifyRules, transform } from 'jsdoc-type-pratt-parser'

const result = jtpTransform(parse('myType.<string>', 'closure'))
const rules = stringifyRules()

// `result` is the current node and `transform` is a function to transform child nodes.
rules.NAME = (result, transform) => 'something else'

const val = transform(rules, { type: 'JsdocTypeName', value: 'name'}) // -> 'something else'
```

Stringify:
You can also build your own transform rules by implementing the [`TransformRules<TransformResultType>`](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#TransformRules) interface or you
can build upon the [identity ruleset](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#identityTransformRules) like this:

```js
import { stringify } from 'jsdoc-type-pratt-parser'
import { identityTransformRules, transform } from 'jsdoc-type-pratt-parser'

const val = stringify({ type: 'JsdocTypeName', value: 'name'}) // -> 'name'
const myRules = identityTransformRules()
myRules.NAME = () => ({ type: 'JsdocTypeName', value: 'funky' })

const val = transform(myRules, result)
```

You can customize the stringification by using `stringifyRules` and `transform`:
This library also supports compatibility modes for catharsis and jsdoctypeparser. The provided transform functions attempt to
transform the output to the expected output of the target library. This will not always be the same as some types are
parsed differently. These modes are thought to make transition easier, but it is advised to use the native output as
this will be more uniform and will contain more information.

[Catharsis compat mode](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#catharsisTransform):

```js
import { stringifyRules, transform } from 'jsdoc-type-pratt-parser'
import { parse, catharsisTransform } from 'jsdoc-type-pratt-parser'

const rules = stringifyRules()
const result = catharsisTransform(parse('myType.<string>', 'closure'))
```

// `result` is the current node and `transform` is a function to transform child nodes.
rules.NAME = (result, transform) => 'something else'
[Jsdoctypeparser compat mode](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#jtpTransform):

const val = transform(rules, { type: 'JsdocTypeName', value: 'name'}) // -> 'something else'
```js
import { parse, jtpTransform } from 'jsdoc-type-pratt-parser'

const result = jtpTransform(parse('myType.<string>', 'closure'))
```

You can traverse a result tree with the `traverse` function:
## Traverse

You can traverse an AST with the [`traverse`](https://jsdoc-type-pratt-parser.github.io/jsdoc-type-pratt-parser/docs/modules.html#traverse) function:

```js
import { traverse } from 'jsdoc-type-pratt-parser'
Expand All @@ -104,51 +127,39 @@ function onEnter(node, parent, property) {
traverse({ type: 'JsdocTypeName', value: 'name'}, onEnter, console.log)
```

You can also build your own transform rules by implementing the `TransformRules<TransformResultType>` interface or you
can build upon the identity ruleset like this:

```js
import { identityTransformRules, transform } from 'jsdoc-type-pratt-parser'

const myRules = identityTransformRules()
myRules.NAME = () => ({ type: 'JsdocTypeName', value: 'funky' })

const val = transform(myRules, result)
```

## Tests Status

This parser runs most tests of https://github.com/hegemonic/catharsis and
https://github.com/jsdoctypeparser/jsdoctypeparser. It compares the results of the different parsing libraries. If you
want to find out where the output differs, look in the tests for the comments `// This seems to be an error of ...` or
the `differ` keyword which indicates that differing results are produced.
https://github.com/jsdoctypeparser/jsdoctypeparser. It compares the results of the different parsing libraries. If you
want to find out where the output differs, look in the tests for the comments `// This seems to be an error of ...` or
the `differ` keyword which indicates that differing results are produced.

## Performance

A simple performance [comparision](benchmark/benchmark.js) using [Benchmark.js](https://benchmarkjs.com/) produced the following results:
A simple [performance comparision](benchmark/benchmark.js) using [Benchmark.js](https://benchmarkjs.com/) produced the following results:
```
Testing expression: Name
catharsis x 36,338 ops/sec ±1.10% (1071 runs sampled)
jsdoc-type-pratt-parser x 400,260 ops/sec ±0.87% (1070 runs sampled)
jsdoctypeparser x 61,847 ops/sec ±1.18% (1071 runs sampled)
catharsis x 37,816 ops/sec ±1.22% (1086 runs sampled)
jsdoc-type-pratt-parser x 602,617 ops/sec ±0.16% (1090 runs sampled)
jsdoctypeparser x 53,256 ops/sec ±0.73% (1081 runs sampled)
The fastest was jsdoc-type-pratt-parser
Testing expression: Array<number>
catharsis x 7,969 ops/sec ±1.05% (1079 runs sampled)
jsdoc-type-pratt-parser x 159,001 ops/sec ±0.95% (1074 runs sampled)
jsdoctypeparser x 42,278 ops/sec ±1.01% (1070 runs sampled)
catharsis x 10,124 ops/sec ±0.56% (1084 runs sampled)
jsdoc-type-pratt-parser x 228,660 ops/sec ±0.40% (1084 runs sampled)
jsdoctypeparser x 42,365 ops/sec ±0.60% (1070 runs sampled)
The fastest was jsdoc-type-pratt-parser
Testing expression: { keyA: Type<A | "string val" >, keyB: function(string, B): A }
catharsis x 933 ops/sec ±1.15% (1070 runs sampled)
jsdoc-type-pratt-parser x 29,596 ops/sec ±0.90% (1068 runs sampled)
jsdoctypeparser x 16,206 ops/sec ±1.38% (1055 runs sampled)
catharsis x 1,138 ops/sec ±0.66% (1087 runs sampled)
jsdoc-type-pratt-parser x 46,535 ops/sec ±0.47% (1090 runs sampled)
jsdoctypeparser x 18,291 ops/sec ±0.71% (1084 runs sampled)
The fastest was jsdoc-type-pratt-parser
```

the test uses catharsis without cache, as this is just a simple lookup table that could easily be implemented for any parser.
The benchmark test uses catharsis without cache.

## Development

If you want to contribute see the [Development Guide](DEVELOPMENT.md) to get some pointers. Feel free to create issues if
there is missing information.
there is information missing.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"apidoc": "typedoc src/index.ts --out docs",
"preversion": "npm test",
"prepublishOnly": "npm run build",
"semantic-release": "semantic-release"
"semantic-release": "semantic-release",
"benchmark": "npm run build && node benchmark/benchmark.js"
},
"author": "Simon Seyock (https://github.com/simonseyock)",
"contributors": [
Expand Down
2 changes: 1 addition & 1 deletion src/parslets/predicateParslet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const predicateParslet = composeParslet({
accept: type => type === 'is',
parseInfix: (parser, left) => {
if (left.type !== 'JsdocTypeName') {
throw new UnexpectedTypeError(left, 'Predicate always have to have names on the left side.')
throw new UnexpectedTypeError(left, 'A typescript predicate always has to have a name on the left side.')
}

parser.consume('is')
Expand Down
13 changes: 13 additions & 0 deletions src/traverse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import { NonRootResult } from './result/NonRootResult'
import { RootResult } from './result/RootResult'
import { visitorKeys } from './visitorKeys'

/**
* A node visitor function.
* @param node the visited node.
* @param parentNode the parent node.
* @param property the property on the parent node that contains the visited node. It can be the node itself or
* an array of nodes.
*/
type NodeVisitor = (node: NonRootResult, parentNode?: NonRootResult, property?: string) => void

function _traverse<T extends NonRootResult, U extends NonRootResult> (node: T, parentNode?: U, property?: keyof U, onEnter?: NodeVisitor, onLeave?: NodeVisitor): void {
Expand All @@ -27,6 +34,12 @@ function _traverse<T extends NonRootResult, U extends NonRootResult> (node: T, p
onLeave?.(node, parentNode, property as string)
}

/**
* A function to traverse an AST. It traverses it depth first.
* @param node the node to start traversing at.
* @param onEnter node visitor function that will be called on entering the node. This corresponds to preorder traversing.
* @param onLeave node visitor function that will be called on leaving the node. This corresponds to postorder traversing.
*/
export function traverse (node: RootResult, onEnter?: NodeVisitor, onLeave?: NodeVisitor): void {
_traverse(node, undefined, undefined, onEnter, onLeave)
}

0 comments on commit bf17935

Please sign in to comment.