Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/loilo/gyros
🥙 Transform PHP ASTs the easy way
https://github.com/loilo/gyros
ast manipulation php
Last synced: 4 months ago
JSON representation
🥙 Transform PHP ASTs the easy way
- Host: GitHub
- URL: https://github.com/loilo/gyros
- Owner: loilo
- License: other
- Created: 2023-05-08T06:41:06.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2024-04-09T04:54:07.000Z (11 months ago)
- Last Synced: 2024-04-14T10:16:05.304Z (10 months ago)
- Topics: ast, manipulation, php
- Language: TypeScript
- Homepage:
- Size: 370 KB
- Stars: 4
- Watchers: 3
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
![]()
Gyros
Transform PHP [AST](http://en.wikipedia.org/wiki/Abstract_syntax_tree)s the easy way. Just as tasty as [Yufka](https://github.com/loilo/yufka), but with different ingredients.
[](https://github.com/loilo/gyros/actions)
[](https://npmjs.com/package/gyros)## Installation
```
npm install --save gyros
```**IMPORTANT:** Gyros is ESM-only. [Read more.](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
## Motivation
Gyros is the ideal tool for programmatically making small & simple modifications to your PHP code in JavaScript.As an introducing example, let's put a function wrapper around all array literals:
```js
import { gyros } from 'gyros'const source = `
$xs = [1, 2, [3, 4]];
$ys = [5, 6];
var_dump([$xs, $ys]);
`const result = gyros(source, (node, { update, source }) => {
if (node.kind === 'array') {
update(`fun(${source()})`)
}
})console.log(result.toString())
```Output:
```php
$xs = fun([1, 2, fun([3, 4])]);
$ys = fun([5, 6]);
var_dump(fun([$xs, $ys]));
```## Usage
### How it Works
```ts
function gyros(source, options = {}, manipulator)
```Transform the string `source` with the function `manipulator`, returning an output object.
For every node in the AST, `manipulator(node, helpers)` fires. The recursive walk is an
in-order traversal, so children get called before their parents. This makes it easier to write nested transforms since transforming parents often requires transforming their children first anyway.The `gyros()` return value is an object with two properties:
* `code` – contains the transformed source code
* `map` – holds the resulting source map object, [as generated by `magic-string`](https://www.npmjs.com/package/magic-string#sgeneratemap-options-)Calling `.toString()` on a Gyros result object will return its source `code`.
> **Pro Tip:**
> Don't know how a PHP AST looks like? Have a look at [astexplorer.net](https://astexplorer.net/) to get an idea.### Options
All options are, as the name suggests, optional. If you want to provide an options object, its place is between the `source` code and the `manipulator` function.#### Parse Mode
There are two parse modes available: `code` and `eval`. The default is `eval`.The `code` parse mode allows to parse PHP code as it appears "in the wild", i.e. with enclosing `= "Hello World!" ?>', { parseMode: 'code' }, (node, helpers) => {
// Parse the `source` as mixed HTML/PHP code
})
```#### PHP Parser Options
Any options for the underlying [`php-parser`](https://npmjs.com/package/php-parser) can be passed to `options.phpParser`:```js
gyros(source, { phpParser: { parser: { suppressErrors: true } } }, (node, helpers) => {
// Parse the `source` in loose mode
})
```#### Source Maps
Gyros uses [`magic-string`](https://www.npmjs.com/package/magic-string) under the hood to generate [source maps](https://developer.mozilla.org/docs/Tools/Debugger/How_to/Use_a_source_map) for your code modifications. You can pass its [source map options](https://www.npmjs.com/package/magic-string#sgeneratemap-options-) as `options.sourceMap`:```js
gyros(source, { sourceMap: { hires: true } }, (node, helpers) => {
// Create a high-resolution source map
})
```### Helpers
The `helpers` object passed to the `manipulator` function exposes the following methods. All of these methods handle the *current AST node* (the one that's passed to the manipulator as its first argument).However, all of these methods take an AST node as an optional first parameter if you want to access other nodes.
> **Example:**
> ```js
> gyros('$x = 1', (node, { source }) => {
> if (node.kind === 'assign') {
> // `node` refers to the `$x = 1` Expression
> source() // returns "$x = 1"
> source(node.right) // returns "1"
> }
> })
> ```#### `source()`
Return the source code for the given node, including any modifications made to
child nodes:```js
gyros('(true)', (node, { source, update }) => {
if (node.kind === 'boolean') {
source() // returns "true"
update('false')
source() // returns "false"
}
})
```#### `update(replacement)`
Replace the source of the affected node with the `replacement` string:```js
const result = gyros('4 + 2', (node, { source, update }) => {
if (node.kind === 'bin') {
update(source(node.left) + source(node.right))
}
})console.log(result.toString())
```Output:
```php
42
```#### `parent(levels = 1)`
From the starting node, climb up the syntax tree `levels` times. Getting an ancestor node of the program root yields `undefined`.```js
gyros('$x = [1]', (node, { parent }) => {
if (node.kind === 'number') {
// `node` refers to the `1` number literal
parent() // same as parent(1), refers to the `1` array item
parent(2) // refers to the `[1]` expression
parent(3) // refers to the `x = [1]` assignment expression
parent(4) // refers to the `x = [1]` statement
parent(5) // refers to the program as a whole (root node)
parent(6) // yields `undefined`, same as parent(7), parent(8) etc.
}
})
```#### External Helper Access
**Tip:** If you want to extract manipulation behavior into standalone functions, you can access the helpers directly on the `gyros` instance (e.g. `gyros.source()`) where they are not bound to a specific node:
```js
// Standalone function, increments node's value if it's a number
const increment = node => {
if (node.kind === 'number') {
gyros.update(node, String(Number(node.value) + 1))
}
}const result = gyros('$x = 1', node => {
increment(node)
})console.log(result.toString())
```
Output:
```php
$x = 2
```### Asynchronous Manipulations
The `manipulator` function may return a [Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise). If it does, Gyros will wait for that to resolve, making the whole `gyros()` function return a Promise resolving to the result object (instead of returning the result object directly):```js
import got from 'got' // see www.npmjs.com/package/gotconst source = `
$content = curl("https://example.com")
`
const deferredResult = gyros(source, async (node, { source, update }) => {
if (node.kind === 'call' && node.what.kind === 'name' && node.what.name === 'curl') {
// Replace all curl() calls with their actual content// Get the URL (will only work for simple string literals)
const url = node.arguments[0].value// Fetch the URL's contents
const contents = (await got(url)).body// Replace the cUrl() call with the fetched contents
update(JSON.stringify(contents))
}
})// Result is not available immediately, we need to await it
deferredResult.then(result => {
console.log(result.toString())
})
```Output:
```php
$content = "\n\n[...]\n"
```> **Note:** You *have* to return a promise if you want to commit updates asynchronously. Once the manipulator function is done running, any `update()` calls originating from it will throw an error.