277 lines
8.2 KiB
Go
277 lines
8.2 KiB
Go
//go:build goexperiment.jsonv2
|
||
|
||
package doc
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json/jsontext"
|
||
"io"
|
||
"sort"
|
||
)
|
||
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
// TokenKind — JSON token 类型。
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
// TokenKind 表示 JSON token 类型。
|
||
type TokenKind int
|
||
|
||
const (
|
||
TokenObjectStart TokenKind = iota // {
|
||
TokenObjectEnd // }
|
||
TokenArrayStart // [
|
||
TokenArrayEnd // ]
|
||
TokenString // "..."
|
||
TokenNumber // 42, 3.14
|
||
TokenTrue // true
|
||
TokenFalse // false
|
||
TokenNull // null
|
||
TokenEOF // 输入结束
|
||
)
|
||
|
||
func (k TokenKind) String() string {
|
||
switch k {
|
||
case TokenObjectStart:
|
||
return "{"
|
||
case TokenObjectEnd:
|
||
return "}"
|
||
case TokenArrayStart:
|
||
return "["
|
||
case TokenArrayEnd:
|
||
return "]"
|
||
case TokenString:
|
||
return "string"
|
||
case TokenNumber:
|
||
return "number"
|
||
case TokenTrue:
|
||
return "true"
|
||
case TokenFalse:
|
||
return "false"
|
||
case TokenNull:
|
||
return "null"
|
||
case TokenEOF:
|
||
return "EOF"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// Token 表示一个带位置的 JSON token。
|
||
type Token struct {
|
||
Kind TokenKind
|
||
Value []byte // 原始字节(string 含引号, number 含原始文本)
|
||
Range Range // 精确源位置
|
||
}
|
||
|
||
// StringValue 返回解码后的 string 值。仅 TokenString 有效。
|
||
// 对于非 string token 返回空串。
|
||
func (t Token) StringValue() string {
|
||
if t.Kind != TokenString || len(t.Value) < 2 {
|
||
return ""
|
||
}
|
||
// Value 含引号,去掉首尾 "
|
||
raw := t.Value
|
||
// 简单路径:如果没有转义符,直接截取
|
||
hasEscape := false
|
||
for _, b := range raw[1 : len(raw)-1] {
|
||
if b == '\\' {
|
||
hasEscape = true
|
||
break
|
||
}
|
||
}
|
||
if !hasEscape {
|
||
return string(raw[1 : len(raw)-1])
|
||
}
|
||
// 有转义符:使用 jsontext.AppendUnquote
|
||
unquoted, err := jsontext.AppendUnquote(nil, raw)
|
||
if err != nil {
|
||
return string(raw[1 : len(raw)-1])
|
||
}
|
||
return string(unquoted)
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
// Lexer — 封装 jsontext.Decoder,提供位置感知的 token 流。
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
// Lexer 封装 jsontext.Decoder,提供位置感知的 token 流。
|
||
type Lexer struct {
|
||
dec *jsontext.Decoder
|
||
src []byte
|
||
lineOffsets []int64 // 每行起始 byte offset (lineOffsets[0] = 0 代表第 1 行)
|
||
diags DiagnosticList
|
||
done bool // EOF 已到达
|
||
}
|
||
|
||
// NewLexer 创建 Lexer 实例。预扫描换行符以建立行号索引。
|
||
func NewLexer(src []byte) *Lexer {
|
||
l := &Lexer{
|
||
dec: jsontext.NewDecoder(bytes.NewReader(src)),
|
||
src: src,
|
||
lineOffsets: buildLineOffsets(src),
|
||
}
|
||
return l
|
||
}
|
||
|
||
// buildLineOffsets 扫描 src 中的 '\n',返回每行的起始 byte offset。
|
||
// lineOffsets[0] = 0(第 1 行从 offset 0 开始)。
|
||
func buildLineOffsets(src []byte) []int64 {
|
||
offsets := []int64{0}
|
||
for i, b := range src {
|
||
if b == '\n' {
|
||
offsets = append(offsets, int64(i+1))
|
||
}
|
||
}
|
||
return offsets
|
||
}
|
||
|
||
// offsetToPosition 将 byte offset 转换为 Position (line/col 均 1-based)。
|
||
func (l *Lexer) offsetToPosition(offset int64) Position {
|
||
// 二分搜索:找到最后一个 <= offset 的 lineOffset 索引
|
||
line := sort.Search(len(l.lineOffsets), func(i int) bool {
|
||
return l.lineOffsets[i] > offset
|
||
})
|
||
// line 是第一个 > offset 的索引,所以实际行号是 line(1-based)
|
||
col := int(offset-l.lineOffsets[line-1]) + 1
|
||
return Position{Line: line, Col: col, Offset: offset}
|
||
}
|
||
|
||
// Next 返回下一个 Token。
|
||
// 遇到 JSON 词法错误时记录 Diagnostic 并返回 TokenEOF。
|
||
func (l *Lexer) Next() (Token, error) {
|
||
if l.done {
|
||
return Token{Kind: TokenEOF, Range: l.eofRange()}, nil
|
||
}
|
||
|
||
beforeOffset := l.dec.InputOffset()
|
||
|
||
tok, err := l.dec.ReadToken()
|
||
if err != nil {
|
||
l.done = true
|
||
if err == io.EOF {
|
||
return Token{Kind: TokenEOF, Range: l.eofRange()}, nil
|
||
}
|
||
// JSON 词法错误
|
||
pos := l.offsetToPosition(beforeOffset)
|
||
l.diags = append(l.diags, Diagnostic{
|
||
Range: Range{Start: pos, End: pos},
|
||
Severity: SeverityError,
|
||
Code: classifyJsontextError(err),
|
||
Source: SourceJSONSyntax,
|
||
Message: err.Error(),
|
||
})
|
||
return Token{Kind: TokenEOF, Range: Range{Start: pos, End: pos}}, err
|
||
}
|
||
|
||
afterOffset := l.dec.InputOffset()
|
||
|
||
// 跳过 beforeOffset 到实际 token 之间的空白,得到精确 token 起始位置
|
||
tokenStart := skipJSONWhitespace(l.src, beforeOffset)
|
||
tokenRange := Range{
|
||
Start: l.offsetToPosition(tokenStart),
|
||
End: l.offsetToPosition(afterOffset),
|
||
}
|
||
|
||
kind := mapJsontextKind(tok.Kind())
|
||
var value []byte
|
||
if tokenStart < afterOffset {
|
||
value = cloneBytes(l.src, tokenStart, afterOffset)
|
||
}
|
||
|
||
return Token{Kind: kind, Value: value, Range: tokenRange}, nil
|
||
}
|
||
|
||
// Diagnostics 返回 Layer 0 收集的所有诊断。
|
||
func (l *Lexer) Diagnostics() DiagnosticList {
|
||
return l.diags
|
||
}
|
||
|
||
// eofRange 返回源文本末尾的 Range。
|
||
func (l *Lexer) eofRange() Range {
|
||
pos := l.offsetToPosition(int64(len(l.src)))
|
||
return Range{Start: pos, End: pos}
|
||
}
|
||
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
// 辅助函数
|
||
// ──────────────────────────────────────────────────────────────────────────────
|
||
|
||
// mapJsontextKind 将 jsontext.Kind 映射到 TokenKind。
|
||
func mapJsontextKind(k jsontext.Kind) TokenKind {
|
||
switch k {
|
||
case '{':
|
||
return TokenObjectStart
|
||
case '}':
|
||
return TokenObjectEnd
|
||
case '[':
|
||
return TokenArrayStart
|
||
case ']':
|
||
return TokenArrayEnd
|
||
case '"':
|
||
return TokenString
|
||
case '0':
|
||
return TokenNumber
|
||
case 't':
|
||
return TokenTrue
|
||
case 'f':
|
||
return TokenFalse
|
||
case 'n':
|
||
return TokenNull
|
||
default:
|
||
return TokenEOF
|
||
}
|
||
}
|
||
|
||
// classifyJsontextError 尝试从错误信息中推断错误码。
|
||
func classifyJsontextError(err error) string {
|
||
msg := err.Error()
|
||
switch {
|
||
case contains(msg, "unterminated string"):
|
||
return CodeJSONSyntaxUnterminatedStr
|
||
case contains(msg, "escape"):
|
||
return CodeJSONSyntaxInvalidEscape
|
||
case contains(msg, "trailing"):
|
||
return CodeJSONSyntaxTrailingData
|
||
default:
|
||
return CodeJSONSyntaxUnexpectedToken
|
||
}
|
||
}
|
||
|
||
// skipJSONWhitespace 从 offset 开始跳过 JSON 空白字符 (space/tab/LF/CR) 和结构分隔符 (,/:)。
|
||
// jsontext.Decoder 内部消费逗号和冒号但不作为 token 返回,
|
||
// 所以 InputOffset() 可能停在上一个 token 后的逗号/冒号位置。
|
||
func skipJSONWhitespace(src []byte, offset int64) int64 {
|
||
for offset < int64(len(src)) {
|
||
b := src[offset]
|
||
if b != ' ' && b != '\t' && b != '\n' && b != '\r' && b != ',' && b != ':' {
|
||
break
|
||
}
|
||
offset++
|
||
}
|
||
return offset
|
||
}
|
||
|
||
// contains 是 strings.Contains 的内联版本(避免额外 import 开销对 hot path 的影响)。
|
||
func contains(s, substr string) bool {
|
||
return len(s) >= len(substr) && searchSubstr(s, substr)
|
||
}
|
||
|
||
func searchSubstr(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// cloneBytes 从 src 中复制 [start, end) 区间的字节。
|
||
func cloneBytes(src []byte, start, end int64) []byte {
|
||
if start < 0 || end > int64(len(src)) || start >= end {
|
||
return nil
|
||
}
|
||
b := make([]byte, end-start)
|
||
copy(b, src[start:end])
|
||
return b
|
||
}
|