{"id":13725313,"url":"https://github.com/lukeed/astray","last_synced_at":"2025-10-09T18:23:40.294Z","repository":{"id":41872957,"uuid":"266230784","full_name":"lukeed/astray","owner":"lukeed","description":"Walk an AST without being led astray","archived":false,"fork":false,"pushed_at":"2023-10-16T17:39:59.000Z","size":822,"stargazers_count":184,"open_issues_count":3,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-07T10:55:14.583Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukeed.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"license","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2020-05-23T00:02:39.000Z","updated_at":"2024-09-29T23:43:21.000Z","dependencies_parsed_at":"2024-02-04T08:48:36.884Z","dependency_job_id":"f10413e9-4645-4adc-b80e-a3dfbf0878a9","html_url":"https://github.com/lukeed/astray","commit_stats":{"total_commits":64,"total_committers":2,"mean_commits":32.0,"dds":0.015625,"last_synced_commit":"017484ce67402224304836e7d1a2fe2e116c3ae9"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fastray","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fastray/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fastray/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukeed%2Fastray/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukeed","download_url":"https://codeload.github.com/lukeed/astray/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224152787,"owners_count":17264764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T01:02:19.174Z","updated_at":"2025-10-09T18:23:35.245Z","avatar_url":"https://github.com/lukeed.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"# astray [![CI](https://github.com/lukeed/astray/workflows/CI/badge.svg)](https://github.com/lukeed/astray/actions) [![codecov](https://badgen.net/codecov/c/github/lukeed/astray)](https://codecov.io/gh/lukeed/astray)\n\n\u003e A tiny (1.01 kB) and [fast](#benchmarks) utility to walk an AST without being led astray.\n\n## Install\n\n```\n$ npm install --save astray\n```\n\n\n## Usage\n\n```js\nimport { parse } from 'meriyah';\nimport * as astray from 'astray';\n\nconst AST = parse(`\n  const sum = (a, b) =\u003e a + b;\n\n  function square(a, b) {\n    return a * b;\n  }\n\n  function sqrt(num) {\n    let value = Math.sqrt(num);\n    console.log('square root is:', value);\n    return value;\n  }\n`)\n\nlet ref, STATE = new Map;\n\n// Walk AST and find `let value` reference\nastray.walk(AST, {\n  Identifier(node, state) {\n    if (node.name === 'value') {\n      ref = node;\n    } else if (node.name === 'Math') {\n      state.set('Math', true);\n    }\n  },\n  FunctionDeclaration: {\n    enter(node, state) {\n      state.set('Math', false);\n    },\n    exit(node, state) {\n      console.log(`\"${node.id.name}\" used Math?`, state.get('Math'));\n    }\n  }\n}, STATE);\n\n//=\u003e \"square\" used Math? false\n//=\u003e \"sqrt\" used Math? true\n\n// What does `let value` see?\nconst bindings = astray.lookup(ref);\nfor (let key in bindings) {\n  console.log(`\"${key}\" ~\u003e `, bindings[key]);\n}\n\n//=\u003e \"value\" ~\u003e  { type: 'VariableDeclarator', ... }\n//=\u003e \"sqrt\" ~\u003e  { type: 'FunctionDeclaration', ... }\n//=\u003e \"num\" ~\u003e  { type: 'Identifier', ... }\n//=\u003e \"sum\" ~\u003e  { type: 'VariableDeclarator', ... }\n//=\u003e \"square\" ~\u003e  { type: 'FunctionDeclaration', ... }\n```\n\n\n## API\n\n### astray.walk\u003cT, S, M\u003e(node: T, visitor: Visitor\\\u003cS, M\u003e, state?: S, parent?: any)\nType: `Function`\u003cbr\u003e\nReturns: `Path\u003cT\u003e` or `T` or `undefined`\n\nBegin traversing an AST starting with `node` and using the `visitor` definition. You may optionally provide a `state` data object that each of the `visitor` methods can access and/or manipulate.\n\nYou may also define a `parent`, if known, for the starting `node`; however, this will likely be unknown most of the time.\n\nIf `node` is falsey, then `astray.walk` returns nothing.\u003cbr\u003e\nIf `node` is not an object, then the `node` itself is returned.\u003cbr\u003e\nOtherwise, any other object/array value will be traversed and returned with an added [`Path`](#path-context) context.\n\n#### node\nType: `any`\n\nThe walker's starting `node`. Its children will be traversed recursively against the `visitor` definition.\n\n#### visitor\nType: `Visitor`\n\nThe defined behavior for traversal. See [Visitors](#visitors) for more.\n\n#### state\nType: `any`\u003cbr\u003e\nRequired: `false`\n\nAny state data to be shared or manipulated during traversal.\nWhen defined, all [Visitors](#visitors) will have access to this value.\n\n#### parent\nType: `any`\u003cbr\u003e\nRequired: `false`\n\nThe `node`'s parent, if known.\n\n\u003e **Note:** You will likely never need to define this!\u003cbr\u003eIn fact, `astray.walk` is recursive and sets/tracks this value as part of each node's [Path Context](#path-context).\n\n\n### astray.lookup\u003cM, T\u003e(node: T, target?: string)\nType: `Function`\u003cbr\u003e\nReturns: `Record\u003cstring, any\u003e`\n\nFind all [bindings](#scopes) that are accessible to this `node` by scaling its ancestry.\n\nWhile doing so, each _parent_ context container (eg, `BlockStatement`, `FunctionDeclaration`, or `Program`) is assigned its own cache of available bindings. See [Path Context](#path-context) for more.\n\nA dictionary of scopes are returned for the `node`. This will be an object whose keys are the identifier names and whose values are references to the nodes that the identifier points to.\n\n\u003e **Note:** The return object will always include the `node` itself.\n\n#### node\nType: `any`\n\nThe starting point \u0026mdash; the node that's interested in learning what's available to it.\n\n#### target\nType: `string`\u003cbr\u003e\nRequired: `false`\n\nAn optional target value that, if found, will immediately exit the ancestral lookup.\u003cbr\u003eThis should be the name of an identifier that your `node` is interested in, or the name of a parent container that you don't wish to exit.\n\n\n### astray.SKIP\nType: `Boolean`\n\nAny [Visitor](#visitors) may return this value to skip traversal of the current node's children.\n\n\u003e **Important:** Trying to `SKIP` from an `exit()` block will have no effect.\n\n\n### astray.REMOVE\nType: `Boolean`\n\nAny [Visitor](#visitors) may return this value to remove this node from the tree.\n\n\u003e **Important:** When the visitor's `exit()` block returns `REMOVE`, the node's children have already been walked.\u003cbr\u003eOtherwise, returning `REMOVE` from `enter()` or the named/base block will skip children traversal.\n\n\n## Visitors\n\nA \"visitor\" is a definition of behaviors/actions that should be invoked when a matching node's `type` is found.\n\nThe visitor keys can be of _any_ (string) value – it's whatever types you expect to see!\u003cbr\u003eBy default, `astray` assumes you're dealing with the [ESTree format](https://github.com/estree/estree) (which is why the examples and TypeScript definitions reference ESTree types) but you are certainly not limited to this specification.\n\nFor example, if you want to target any `VariableDeclaration` nodes, you may do so like this:\n\n```js\nconst STATE = {};\n\n// via method\nastray.walk(tree, {\n  VariableDeclaration(node, state) {\n    // I entered `VariableDeclaration` node\n    assert.is(state === STATE, true);\n  }\n});\n\n// via enter/exit hooks\nastray.walk(tree, {\n  VariableDeclaration: {\n    enter(node, state) {\n      // I entered `VariableDeclaration` node\n      assert.is(state === STATE, true);\n    },\n    exit(node, state) {\n      // I exited `VariableDeclaration` node\n      assert.is(state === STATE, true);\n    }\n  }\n});\n```\n\nAs you can see, the object-variant's `enter()` block is synonymous with the method-variant. (For simplicity, both formats will be referred to as the \"enter\" block.) However, an `exit` may only exist within the object-variant, forcing an existing method-variant to be converted into an `enter` key. When using the object-variant, the `enter` and `exit` keys are both optional – but at least one should exist, of course.\n\nRegardless of the visitor's format, every method has access to the _current_ `node` value as its first parameter. This is direct access to the tree's child, so any modification will mutate the value directly. Additionally, if you provided [`astray.walk()`](##astraywalkt-snode-t-visitor-visitors-state-s-parent-any) with a [`state`](#state) value, that `state` is also passed to each visitor. This, too, allows you to directly mutate/modify your state object.\n\nAnything that happens within the \"enter\" block happens _before_ the node's children are traversed. In other words, you _may_ alter the fate of this node's children. For example, returning the [`SKIP`](#astrayskip) or [`REMOVE`](#astrayremove) signals prevent your walker from ever seeing the children.\n\nAnything that happens within the \"exit\" block happens _after_ the node's children have been traversed. For example, because `state` is shared, you can use this opportunity to collect any `state` values/flags that the children may have provided. Again, since child traversal has already happened, returning the [`SKIP`](#astrayskip) signal has no effect. Additionally, returning the [`REMOVE`](#astrayremove) signal still remove the `node` and its children, but still allows you to know what _was_ there.\n\n## Path Context\n\nAny objects seen during traversal (`astray.walk`), even those that had no matching Visitors, receive a new `path` key. This is known as the \"path context\" – and will _always_ have a `parent` key.\n\nIn cases where a `node` does not have a parent (eg, a `Program`), then `node.path.parent` will exist with `undefined` value.\n\nWhen scaling a `node`'s ancestry (`astray.lookup`), additional keys are added to its **parents'** contexts:\n\n* **scoped** \u0026mdash; a dictionary of [bindings](#scopes) _owned by_ this node's context;\n* **bindings** \u0026mdash; a dictionary of [_all bindings_](#scopes) _accessible by_ this node, including its own;\n* **scanned** \u0026mdash; a `boolean` indicating that the `bindings` dictionary is complete; aka, has seen all parents\n\n\u003e **Important:** Only **parent** contexts contain scope information. \u003cbr\u003eThese include `BlockStatement`, `FunctionDeclaration`, and `Program` node types.\n\n## Scopes\n\nWhen using [`astray.lookup()`](#astraylookupnode-t-target-string), path contexts _may_ obtain scope/binding information. \u003cbr\u003eThese are records of what each parent container _provides_ (`node.path.scoped`) as well as what is _accessible_ (`node.path.bindings`) to this scope level. Additionally, if a node/parent's _entire_ ancestry has been recorded, then `node.path.scanned` will be true.\n\nThe records of bindings (including `astray.lookup`'s return value) are objects keyed by the identifier names. The keys' values are references to the node that included/defined that identifier. For example, this means that `VariableDeclarator`s will be returned instead of the `VariableDeclaration` that contained them. You may still access the `VariableDeclaration` via the `VariableDeclarator`s path context (`node.path.parent`).\n\nHere's a simple example:\n\n```js\nimport { parse } from 'meriyah';\nimport * as astray from 'astray';\n\nconst source = `\n  const API = 'https://...';\n\n  function send(url, isGET) {\n    console.log('method:', isGET ? 'GET' : 'POST');\n    console.log('URL:', API + url);\n  }\n\n  function Hello(props) {\n    var foobar = props.url || '/hello';\n    send(foobar, true)\n  }\n`;\n\nlet foobar;\nconst AST = parse(source);\n\n// walk \u0026 find `var foobar`\nastray.walk(AST, {\n  Identifier(node) {\n    if (node.name === 'foobar') {\n      foobar = node; // save reference\n    }\n  }\n});\n\n// get everything `foobar` can see\nconst bindings = astray.lookup(foobar);\n\nfor (let key in bindings) {\n  console.log(key, bindings[key].type);\n}\n\n//=\u003e foobar VariableDeclarator\n//=\u003e Hello FunctionDeclaration\n//=\u003e props Identifier\n//=\u003e API VariableDeclarator\n//=\u003e send FunctionDeclaration\n```\n\n\n## Benchmarks\n\n\u003e Running on Bun 1.2.3\n\n***Load Time***\n\nHow long does it take to `require` the dependency?\n\n```\n@babel/traverse:   52.72ms\nestree-walker:      0.66ms\nacorn-walk:         0.75ms\nast-types:         12.08ms\nastray:             0.37ms\n```\n\n***Walking***\n\nAll candidates traverse the pre-parsed AST (ESTree format, unless noted otherwise) of `d3.min.js`. \u003cbr\u003eEach candidate must count the `Identifier` nodes seen as a validation step.\n\n```\nValidation:\n  ✔ @babel/traverse ≠   (41,669 identifiers)\n  ✔ estree-walker       (41,669 identifiers)\n  ✘ acorn-walk †        (23,340 identifiers)\n  ✔ ast-types           (41,669 identifiers)\n  ✔ astray              (41,669 identifiers)\n\nBenchmark:\n  @babel/traverse    x  38 ops/sec ±1.65% (52 runs sampled)\n  estree-walker      x 248 ops/sec ±0.12% (92 runs sampled)\n  acorn-walk †       x 172 ops/sec ±0.05% (89 runs sampled)\n  ast-types          x  16 ops/sec ±1.62% (46 runs sampled)\n  astray             x 301 ops/sec ±0.14% (92 runs sampled)\n```\n\n\u003e **Notice:**\u003cbr\u003e\u003cbr\u003e\n\u003e Run `$ cat bench/fixtures/estree.json | grep \"Identifier\" | wc -l` to verify the `41,669` figure.\u003cbr\u003e\u003cbr\u003e\n\u003e \u003csup\u003e`≠`\u003c/sup\u003e Babel does not follow the ESTree format. Instead `@babel/traverse` requires that `@babel/parser` be used in order for validation to pass.\u003cbr\u003e\u003cbr\u003e\n\u003e \u003csup\u003e`†`\u003c/sup\u003e Acorn _does_ follow the ESTree format, but `acorn-walk` still fails to count all identifiers. All exported methods (simple, full, recursive) returned the same value. Results are taken using an `acorn` AST, although it fails using while traversing the ESTree fixture (`estree.json`).\n\n## License\n\nMIT © [Luke Edwards](https://lukeed.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeed%2Fastray","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukeed%2Fastray","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukeed%2Fastray/lists"}