Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Parser Internals

ASecurity

understand, debug, or extend the cfmleditor-lsp parser — scanner tokenisation, the two parse loops, call-site extraction, and how the unresolved command works

4 stars
0 votes
0 copies
1 views
Added 9/20/2026
code-qualitygoexpressdebugginggitapi

Works with

cliapi

Security Analysis

A100/100

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add cfmleditor/cfmleditor-lsp --skill parser-internals --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Parser Internals?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Parser Internals
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/cfmleditor-parser-internals/badge)](https://www.skillsdirectory.com/skills/cfmleditor-parser-internals)

More formats (shields.io, HTML) on the badges page.

Download with Pro
Files
SKILL.md
---
name: parser-internals
description: understand, debug, or extend the cfmleditor-lsp parser — scanner tokenisation, the two parse loops, call-site extraction, and how the unresolved command works
---

Use this skill when debugging unexpected parser output, tracing how a CFML construct is tokenised, adding call-site extraction features, or investigating why a variable/ref is wrong in the `unresolved` command output.

## Scanner (`internal/parser/scanner.go`)

The scanner is byte-level. Two predicates control what counts as an identifier:

```go
func isIdentStart(ch byte) bool {
    return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_' || ch == '$'
}
func isIdentPart(ch byte) bool { return isIdentStart(ch) || isDigit(ch) }
```

`$` is a valid identifier start — `$assert` tokenises as a single `TokIdent("$assert")`. If you see a `$`-prefixed variable being stripped in output, the scanner was the first place to check (it lacked `$` before the fix in the current working tree).

`charToKind` maps single characters to token kinds. Anything not matched there falls through as `TokOther` and is silently discarded by the parsers' `TokIdent`-only loops.

## Two parse loops

### 1. Top-level loop — `scriptParser.parse()` (`script_parser.go:475`)

Runs over global/component scope. The `default:` arm handles dot-chains:

```
tok (ident) → peek == TokDot → walk chain → peek == TokLParen
  → if extractCalls: addCall{Variable: chain[:lastDot], FuncName: lastIdent}
```

### 2. Function body loop — `scriptParser.parseBody()` → `handleBodyToken()` (`script_parser.go:866, 989`)

`parseBody` drives `handleBodyToken` for every `TokIdent` inside `{ }`. The `default:` arm:

```
tok (ident) → peek == TokLParen  → recordBareCallAndChain (bare call)
tok (ident) → peek == TokDot     → checkAssignRef → checkBareCall
```

`checkBareCall` (`script_parser.go:1623`) walks the dot chain, checks the next peek is `TokLParen`, then records:

```go
chain := "$assert.isEqual"   // fullChain.String()
dotIdx := 7
varName = chain[:dotIdx]     // "$assert"
// CallSite{FuncName: "isEqual", Variable: "$assert", ...}
```

**Key difference from the top-level loop**: `handleBodyToken` only dispatches on the immediate next token. A `$foo.bar()` call goes through `checkAssignRef` → `checkBareCall`, NOT through the dot-chain arm of `parse()`.

## Call-site extraction pipeline

Enable with `ParseOptions{ExtractCalls: true}`.

| Location | Stored in |
|---|---|
| Global / component scope | `pr.Calls` |
| Inside a function | `pr.funcCallsMap["start:end"]` |

`pr.FuncCalls(0, lastLine)` aggregates both when `funcKey(0, lastLine)` is not a real function key — use this in the `unresolved` command to get all calls across the file.

## ComponentRef vs funcRef

| Kind | API |
|---|---|
| Component-scope (`variables.x = new Foo()`) | `pr.ComponentRefs` |
| Function-local (`var x = new Foo()` inside a func) | `pr.FuncComponentRefs(scope.Start, scope.End)` |

`CanResolveCall` in `internal/resolve/resolve.go:277` checks function-scoped refs first, then falls back to `pr.ComponentRefs`. The reason string `"variable 'X' has no component ref"` means neither lookup found a `ComponentRef.Variable` matching `X`.

## The `unresolved` command (`cmd/cfmleditor-lsp/unresolved.go`)

Parse options used:

```go
parser.ParseOptions{
    Resolvers:          cfResolvers,
    ExpressionMappings: expressionMappings,
    ExtractCalls:       true,
    ScanAllScopes:      true,
}
```

Then calls `pr.FuncCalls(0, lastLine)` and feeds each call to `resolver.CanResolveCall`.

`ScanAllScopes` is stored on `ParseResult` but currently has no additional effect beyond `ExtractCalls` — all scopes are already scanned when `ExtractCalls: true`.

## Quick debug checklist

1. **Wrong variable name in output** (e.g. `assert` instead of `$assert`) → check `isIdentStart` in `scanner.go`; the leading character may not be recognised as ident.
2. **Call not captured at all** → confirm `extractCalls: true` is set; check whether the call is at top-level (`parse()`) or inside a function (`handleBodyToken`); trace through `checkBareCall`.
3. **ComponentRef not resolving** → check `pr.ComponentRefs` vs `pr.FuncComponentRefs`; `CanResolveCall` tries function-scoped first.
4. **Rebuild after scanner changes** → `make build` (or `make cfparse` for the debug CLI); the installed binary won't pick up changes automatically.

Attribution

cfmleditorcfmleditor
View sourceMore from cfmleditor →
SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Know which skills are safe — weekly.

Best new skills + every skill we flagged as malicious. From the team that scanned 103,619.

Join free

Related Skills

Caveman Review

Ultra-compressed code review comments. Cuts noise from PR feedback while preserving the actionable signal. Each comment is one line: location, problem, fix. Use when user says "review this PR", "code review", "review the diff", "/review", or invokes /caveman-review. Auto-triggers when reviewing pull requests.

1066601 votes

Caveman Commit

Ultra-compressed commit message generator. Cuts noise from commit messages while preserving intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why" isn't obvious. Use when user says "write a commit", "commit message", "generate commit", "/commit", or invokes /caveman-commit. Auto-triggers when staging changes.

1066601 votes

Springboot Verification

Verification loop for Spring Boot projects: build, static analysis, tests with coverage, security scans, and diff review before release or PR.

2456590 votes

Verification Loop

一个全面的 Claude Code 会话验证系统。

2456590 votes

Django Verification

Verification loop for Django projects: migrations, linting, tests with coverage, security scans, and deployment readiness checks before release or PR.

2456590 votes
View all in code-quality →