# Retire Add Ast Query

> Add an AST query for a JavaScript library to the retire.js vulnerability database. Given a library name, downloads multiple versions, analyzes the code structure where the version string is assigned, writes an ASTronomical XPath-like query anchored with library-specific identifiers, tests the query locally, adds it to jsrepository-ast.js, and runs validate/test-detection. Use this skill whenever adding an AST-based version detection query to retire.js.

- Skill: `retirejs/retire-add-ast-query` (Agent Skill)
- Install (CLI): `npx skillmds@latest add retirejs/retire-add-ast-query`
- Raw SKILL.md: https://api.skillmd.com/api/skills/retirejs/retire-add-ast-query/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: retirejs (https://skillmd.com/u/retirejs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/retirejs/retire-add-ast-query

---


# retire-add-ast-query

This skill adds an ASTronomical query for a JavaScript library to the retire.js AST detection system. It downloads real files from CDNs, reads the code structure around version assignments, builds a query anchored to library-specific identifiers, tests it locally with `astronomical`, and validates with `test-detection.js`.

The repo is at `/Users/erlend/code/github/RetireJS/retire.js`. All file edits and validation commands run there.

## Overview

AST queries detect the library version by traversing the JavaScript AST rather than matching raw text. Queries must be anchored to library-specific identifiers on sibling or parent nodes — otherwise they'll fire on unrelated files that happen to have a `version` assignment. The query returns the version string directly (no `§§version§§` placeholder needed — the Literal node's value is the result).

All queries live in `repository/jsrepository-ast.js` (a CommonJS module). After editing it, run `./convertToVersioned` to propagate to the versioned JSON files used at runtime.

## Step 1 — CDN Lookup

Fetch library metadata from cdnjs:

```
https://api.cdnjs.com/libraries/{NAME}?fields=name,versions,filename,description
```

or jsdelivr if not on cdnjs:

```
https://data.jsdelivr.com/v1/packages/npm/{NAME}
```

Note the `filename` field (minified name) and derive the non-minified name (strip `.min`). Record the full list of versions.

## Step 2 — Select Versions

Pick 4–5 versions spread across history: 1 early (not the very first), 2–3 middle, and the latest.

## Step 3 — Download Files

```bash
mkdir -p /tmp/retire-ast-{NAME}
# For each VERSION:
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/{NAME}/{VERSION}/{MIN_FILENAME}" \
  -o /tmp/retire-ast-{NAME}/{VERSION}.min.js
curl -sL "https://cdnjs.cloudflare.com/ajax/libs/{NAME}/{VERSION}/{UNMIN_FILENAME}" \
  -o /tmp/retire-ast-{NAME}/{VERSION}.js
```

Verify each downloaded successfully (non-zero, not an HTML 404 page):
```bash
ls -la /tmp/retire-ast-{NAME}/
head -3 /tmp/retire-ast-{NAME}/{VERSION}.js
```

If cdnjs returns 404, fall back to jsDelivr:
```bash
curl -sL "https://cdn.jsdelivr.net/npm/{NAME}@{VERSION}/dist/{FILENAME}" \
  -o /tmp/retire-ast-{NAME}/{VERSION}.js
```

## Step 4 — Find Version Strings

Grep each downloaded file for the exact version number:
```bash
grep -n "1\.2\.3" /tmp/retire-ast-{NAME}/{VERSION}.js
grep -n "1\.2\.3" /tmp/retire-ast-{NAME}/{VERSION}.min.js
```

For single-line minified files where grep context flags fail, use Python:
```bash
python3 -c "
import re
with open('/tmp/retire-ast-{NAME}/{VERSION}.min.js', 'r', errors='replace') as f:
    content = f.read()
for m in re.findall(r'.{0,100}1\.2\.3.{0,100}', content)[:5]:
    print(m)
"
```

## Step 5 — Understand the Code Pattern

Read 10–20 lines around each version occurrence in non-minified files:
```bash
grep -n "1\.2\.3" /tmp/retire-ast-{NAME}/{VERSION}.js
# Then:
sed -n '${LINE_MINUS_10},${LINE_PLUS_10}p' /tmp/retire-ast-{NAME}/{VERSION}.js
```

Identify two things:
1. **What node holds the version**: e.g. `x.VERSION = "1.2.3"` (AssignmentExpression → right → Literal), `{version: "1.2.3"}` (ObjectExpression → Property → value → Literal), `var version = "1.2.3"` (VariableDeclarator → init → Literal)
2. **Library-specific anchors nearby**: an identifier that uniquely identifies this library. The anchor must appear in the same block/expression as the version assignment — or inside a method body of the same object.

**Anchor quality, best to worst:**
- **Gold**: an identifier that contains the library name — e.g. `tinyMCEPreInit`, `isMoment`, `HandlebarsEnvironment`, `__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED`. These cannot match any other library by definition.
- **Good**: a highly domain-specific sibling property or method that wouldn't appear together with a version string in any other library — e.g. `migrateVersion`, `focusedEditor` combined with `majorVersion`.
- **Weak**: generic property names like `releaseDate`, `activeEditor`, `major`, `init`, `build`. These could plausibly appear in other libraries and should only be used in combination with other anchors, never alone.

**Look inside method bodies too.** If the version is in an object literal, scan the body of methods on that same object for library-name identifiers — e.g. `setup() { ... window.tinyMCEPreInit ... }` inside the same ObjectExpression. A `//MemberExpression` or `//Identifier` traversal into a method body provides a much stronger anchor than any generic sibling property name.

Ask yourself: *would this query fire on a file from an unrelated library that happens to assign a version string?* If you're not confident the answer is no, strengthen the anchor.

## Step 6 — Write a Candidate Query

Use ASTronomical syntax to navigate from the root to the version Literal, filtering with library-specific anchors.

### ASTronomical syntax quick reference

| Syntax | Meaning |
|--------|---------|
| `//NodeType` | Descendant at any depth |
| `/NodeType` | Direct child |
| `/:attr` | Named attribute of current node |
| `/$:name` | Resolve identifier binding (follow variable reference) |
| `/$$:name` | Binding or attribute fallback (when value may be variable or literal) |
| `[cond]` | Filter; use `&&`, `\|\|`, `==` |
| `../ ` | Go to parent (use sparingly — causes extra traversal) |
| `/fn:concat(sel1, ".", sel2)` | Concatenate results (for `major.minor.patch`) |

### Common patterns

**Named property assignment in a block with a sibling anchor:**
```
//BlockStatement[
  /ExpressionStatement/AssignmentExpression[/:left/:property/:name == "ANCHOR_PROP"]
]/ExpressionStatement/AssignmentExpression[
  /:left/:property/:name == "version"
]/$$:right/:value
```

**Named object property with a co-occurring sibling property:**
```
//ObjectExpression[
  /Property/:key/:name == "ANCHOR_PROP"
]/Property[/:key/:name == "version"]/:value/:value
```

**Assignment where the object is named directly:**
```
//AssignmentExpression[
  /:left/:object/:name == "LibName" &&
  /:left/:property/:name == "VERSION"
]/:right/:value
```

**Two properties sharing the same object binding (stronger anchor):**
```
//SequenceExpression[
  /AssignmentExpression[/:left/:property/:name == "ANCHOR_PROP"]/:left/$:object ==
  /AssignmentExpression[/:left/:property/:name == "version"]/:left/$:object
]/AssignmentExpression[/:left/:property/:name == "version"]/$$:right/:value
```

**Object with library-name identifier inside a method body (gold anchor):**
```
//ObjectExpression[
  /Property[/:key/:name == "majorVersion"] &&
  /Property[/:key/:name == "setup"]//MemberExpression[/:property/:name == "libraryNamePreInit"]
]/fn:concat(
  /Property[/:key/:name == "majorVersion"]/:value/:value,
  ".",
  /Property[/:key/:name == "minorVersion"]/:value/:value
)
```

Start simple, add anchors until the query is specific enough. Multiple queries in the array are OR'd — add a second entry for a different version era if the code structure changed across major versions.

**If two candidate queries end up nearly identical, stop and suspect an ASTronomical bug instead of writing both.** "Nearly identical" means the same filters/anchors, differing only in the terminal extraction step — e.g. one query ends in `/:value` and works against a plain string `Literal` (`x.version = "1.2.3"`), but a real-world build needs a second, otherwise-identical query ending in `/TemplateElement/:value/:cooked` because that build emits the version as a non-interpolated template literal instead (`` x.version = `1.2.3` ``). A `Literal` and a template literal with no `${}` are semantically the same string — the query *engine* should resolve either transparently, not every library's query author. Duplicating the query per literal-style is a local workaround that only fixes the one library, and this exact shape of gap tends to recur (react, react-dom, moment.js, and ua-parser-js all independently hit it in the same session because one bundler happened to prefer backtick strings).

When you notice this, don't add the duplicate to `jsrepository-ast.js`. Instead:
1. Report the pattern to the user and propose fixing it in ASTronomical itself, at `/Users/erlend/code/github/RetireJS/ASTronomical` (a sibling checkout — confirm it exists before assuming the path).
2. The fix belongs in `src/index.ts`'s attribute-resolution helpers (`getPrimitiveChildren` / `getPrimitiveChildrenOrNodePaths`) — add a fallback so a requested attribute (e.g. `value`) transparently resolves through the alternate AST shape, instead of requiring the query to spell out both shapes.
3. Validate with the engine's own suite (`npm run typecheck && npm test` in the ASTronomical repo) before touching retire.js again, and confirm the *original* (single, non-duplicated) query now matches both shapes.
4. Bumping ASTronomical's version, updating its `CHANGELOG.md`, publishing to npm, and bumping the dependency in `node/package.json` are all separate, more consequential steps (a published package affects every consumer, not just this repo) — check with the user before doing any of them rather than assuming the fix should ship immediately.

## Step 7 — Test the Query Locally

Create a test script:
```javascript
// /tmp/test-ast-query.js
const { multiQuery } = require('/Users/erlend/code/github/RetireJS/retire.js/node/node_modules/astronomical');
const fs = require('fs');
const code = fs.readFileSync(process.argv[2], 'utf8');
const query = `YOUR QUERY HERE`;
const results = multiQuery(code, { version: query });
console.log(JSON.stringify(results.version, null, 2));
```

Run against each downloaded version:
```bash
node /tmp/test-ast-query.js /tmp/retire-ast-{NAME}/{VERSION}.js
node /tmp/test-ast-query.js /tmp/retire-ast-{NAME}/{VERSION}.min.js
```

Expected output: a JSON array containing exactly the version string, e.g. `["1.2.3"]`. If the output is `[]`, the query didn't match — revise it. If it returns multiple values, the anchor is too loose.

Iterate across all tested versions until each returns the correct version string. If an early version has a structurally different pattern, write a second query string.

## Step 8 — Add to jsrepository-ast.js

Edit `repository/jsrepository-ast.js`. Add the library key to `exports.queries` (in alphabetical order is nice but not required):

```javascript
"library-name": [
  /*
  Code pattern this query matches, e.g.:
    LibName.isMoment = function; LibName.version = "1.2.3"
  */
  `//SequenceExpression[
    /AssignmentExpression[/:left/:property/:name == "isMoment"]/:left/$:object ==
    /AssignmentExpression[/:left/:property/:name == "version"]/:left/$:object
  ]/AssignmentExpression[/:left/:property/:name == "version"]/$$:right/:value`,
],
```

If the library already has an entry (added by someone else), append to its array rather than replacing it.

## Step 9 — Update testcases.json if needed

If the library is already in `repository/testcases.json`, check if any of the tested versions have a different code structure that the query can't reach. Add `allowAstMiss` for those versions:

```json
"library-name": {
  "https://cdnjs.cloudflare.com/.../§§version§§/...§§subversion§§.js": {
    "versions": ["1.0.0", "2.0.0", "3.0.0"],
    "subversions": ["", ".min"],
    "allowAstMiss": ["1.0.0"]
  }
}
```

If the library is **not yet** in `testcases.json`, add a minimal entry covering the 3–5 tested versions. Without a `testcases.json` entry, `test-detection.js` won't exercise the AST query.

## Step 10 — Validate and Test

```bash
cd /Users/erlend/code/github/RetireJS/retire.js/repository
./convertToVersioned && ./validate
node test-detection.js {LIBRARY_NAME}
node test-detection.js          # full suite — catch any false positives introduced
```

**Fixing failures:**
- `validate` error: check `jsrepository-ast.js` syntax — it's a JS module, not JSON; look for unclosed template literals or missing commas
- `Did not detect ... using ast`: query doesn't match that version — refine the query (back to Step 7) or add `allowAstMiss` for that version. If the fix you're reaching for is "add a near-duplicate query that only changes how the final value is extracted," stop — see the callout in Step 6 about suspecting an ASTronomical engine gap instead.
- `Detect multiple components using ast`: query is too broad — strengthen the anchor (back to Step 6)

## Step 11 — Report Results

Show:
- The final query string(s) added to `jsrepository-ast.js`
- Which versions were successfully detected via AST
- The timing output from `test-detection.js` (C: content ms, B: AST ms per version)

