Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

285 lines
5.7 KiB
Go

package preprocess
import (
"strings"
)
// lexer is a dialect-parameterized scanner over a SQL file. It does not parse
// SQL; it knows just enough about comments, string literals, quoted identifiers
// and dollar-quoted strings to walk a query without ever looking inside a
// region where sqlc syntax must be left alone.
type lexer struct {
d Dialect
src string
}
func isIdentStart(c byte) bool {
return c == '_' ||
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
c >= 0x80
}
func isIdentPart(c byte) bool {
return isIdentStart(c) || (c >= '0' && c <= '9') || c == '$'
}
func isDigit(c byte) bool {
return c >= '0' && c <= '9'
}
func isSpace(c byte) bool {
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v'
}
func (l *lexer) at(i int) byte {
if i < 0 || i >= len(l.src) {
return 0
}
return l.src[i]
}
// skip returns the offset just past the comment, string literal, quoted
// identifier or dollar-quoted string that starts at i. It returns -1 when no
// such token starts at i.
func (l *lexer) skip(i int) int {
switch c := l.at(i); c {
case '\'':
return l.skipQuoted(i, '\'', l.d.Backslash || l.isEscapeString(i))
case '"':
return l.skipQuoted(i, '"', l.d.DoubleQuoteString && l.d.Backslash)
case '`':
if !l.d.Backtick {
return -1
}
return l.skipQuoted(i, '`', false)
case '-':
if l.at(i+1) == '-' {
return l.skipLine(i)
}
case '#':
if l.d.HashComment {
return l.skipLine(i)
}
case '/':
if l.at(i+1) == '*' {
return l.skipBlockComment(i)
}
case '$':
if l.d.DollarQuote {
if end := l.skipDollarQuoted(i); end >= 0 {
return end
}
}
}
return -1
}
// isEscapeString reports whether the string literal starting at i is a
// PostgreSQL escape string constant, written E'...', where a backslash escapes
// the following character.
func (l *lexer) isEscapeString(i int) bool {
if !l.d.DollarQuote { // PostgreSQL-only syntax
return false
}
c := l.at(i - 1)
if c != 'e' && c != 'E' {
return false
}
// The E must not be the tail of a longer identifier.
return !isIdentPart(l.at(i - 2))
}
func (l *lexer) skipQuoted(i int, quote byte, backslash bool) int {
for j := i + 1; j < len(l.src); j++ {
switch l.src[j] {
case '\\':
if backslash {
j++
}
case quote:
// A doubled quote is an escaped quote, not the end.
if l.at(j+1) == quote {
j++
continue
}
return j + 1
}
}
return len(l.src)
}
func (l *lexer) skipLine(i int) int {
if idx := strings.IndexByte(l.src[i:], '\n'); idx >= 0 {
return i + idx + 1
}
return len(l.src)
}
func (l *lexer) skipBlockComment(i int) int {
depth := 0
for j := i; j < len(l.src)-1; j++ {
if l.src[j] == '/' && l.src[j+1] == '*' {
depth++
j++
continue
}
if l.src[j] == '*' && l.src[j+1] == '/' {
depth--
j++
if depth == 0 || !l.d.NestedBlockComment {
return j + 1
}
}
}
return len(l.src)
}
// skipDollarQuoted handles PostgreSQL's $tag$ ... $tag$ string literals. It
// returns -1 when i does not start a dollar quote.
func (l *lexer) skipDollarQuoted(i int) int {
tag := l.dollarTag(i)
if tag == "" {
return -1
}
if idx := strings.Index(l.src[i+len(tag):], tag); idx >= 0 {
return i + len(tag) + idx + len(tag)
}
return len(l.src)
}
// dollarTag returns the full opening delimiter ("$$" or "$tag$") when one
// starts at i, and "" otherwise.
func (l *lexer) dollarTag(i int) string {
if l.at(i) != '$' {
return ""
}
j := i + 1
for j < len(l.src) && l.src[j] != '$' {
c := l.src[j]
if isDigit(c) && j == i+1 {
return "" // $1 is a bind parameter, not a tag
}
if !isIdentPart(c) || c == '$' {
return ""
}
j++
}
if j >= len(l.src) {
return ""
}
return l.src[i : j+1]
}
// identEnd returns the offset just past the identifier that starts at i.
func (l *lexer) identEnd(i int) int {
j := i
for j < len(l.src) && isIdentPart(l.src[j]) {
j++
}
return j
}
// skipSpace advances past whitespace and comments.
func (l *lexer) skipSpace(i int) int {
for i < len(l.src) {
if isSpace(l.src[i]) {
i++
continue
}
if l.src[i] == '-' && l.at(i+1) == '-' {
i = l.skipLine(i)
continue
}
if l.src[i] == '/' && l.at(i+1) == '*' {
i = l.skipBlockComment(i)
continue
}
if l.src[i] == '#' && l.d.HashComment {
i = l.skipLine(i)
continue
}
break
}
return i
}
// statements splits the source into statement spans at top-level semicolons.
// The spans cover the whole source, so every byte belongs to exactly one
// statement.
func (l *lexer) statements() [][2]int {
var out [][2]int
start := 0
for i := 0; i < len(l.src); {
if end := l.skip(i); end >= 0 {
i = end
continue
}
if l.src[i] == ';' {
out = append(out, [2]int{start, i + 1})
start = i + 1
}
i++
}
if start < len(l.src) {
out = append(out, [2]int{start, len(l.src)})
}
return out
}
// matchParen returns the offset of the ")" that closes the "(" at i, or -1.
func (l *lexer) matchParen(i int) int {
depth := 0
for j := i; j < len(l.src); {
if end := l.skip(j); end >= 0 {
j = end
continue
}
switch l.src[j] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
return j
}
}
j++
}
return -1
}
// splitArgs splits the text between parentheses on top-level commas. Empty or
// whitespace-only input yields no arguments.
func (l *lexer) splitArgs(start, end int) []string {
var args []string
depth := 0
last := start
for j := start; j < end; {
if stop := l.skip(j); stop >= 0 {
if stop > end {
stop = end
}
j = stop
continue
}
switch l.src[j] {
case '(':
depth++
case ')':
depth--
case ',':
if depth == 0 {
args = append(args, l.src[last:j])
last = j + 1
}
}
j++
}
tail := l.src[last:end]
if len(args) == 0 && strings.TrimSpace(tail) == "" {
return nil
}
return append(args, tail)
}