snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
171
internal/source/code.go
Normal file
171
internal/source/code.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type Edit struct {
|
||||
Location int
|
||||
Old string
|
||||
New string
|
||||
OldFunc func(string) int
|
||||
}
|
||||
|
||||
type CommentSyntax struct {
|
||||
Dash bool
|
||||
Hash bool
|
||||
SlashStar bool
|
||||
}
|
||||
|
||||
func LineNumber(source string, head int) (int, int) {
|
||||
// Calculate the true line and column number for a query, ignoring spaces
|
||||
var comment bool
|
||||
var loc, line, col int
|
||||
for i, char := range source {
|
||||
loc += 1
|
||||
col += 1
|
||||
// TODO: Check bounds
|
||||
if char == '-' && source[i+1] == '-' {
|
||||
comment = true
|
||||
}
|
||||
if char == '\n' {
|
||||
comment = false
|
||||
line += 1
|
||||
col = 0
|
||||
}
|
||||
if loc <= head {
|
||||
continue
|
||||
}
|
||||
if unicode.IsSpace(char) {
|
||||
continue
|
||||
}
|
||||
if comment {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return line + 1, col
|
||||
}
|
||||
|
||||
func Pluck(source string, location, length int) (string, error) {
|
||||
head := location
|
||||
tail := location + length
|
||||
return source[head:tail], nil
|
||||
}
|
||||
|
||||
func Mutate(raw string, a []Edit) (string, error) {
|
||||
if len(a) == 0 {
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
sort.Slice(a, func(i, j int) bool { return a[i].Location > a[j].Location })
|
||||
|
||||
s := raw
|
||||
for idx, edit := range a {
|
||||
start := edit.Location
|
||||
if start > len(s) || start < 0 {
|
||||
return "", fmt.Errorf("edit start location is out of bounds")
|
||||
}
|
||||
var oldLen int
|
||||
if edit.OldFunc != nil {
|
||||
oldLen = edit.OldFunc(s[start:])
|
||||
} else {
|
||||
oldLen = len(edit.Old)
|
||||
}
|
||||
|
||||
stop := edit.Location + oldLen
|
||||
if stop > len(s) {
|
||||
return "", fmt.Errorf("edit stop location is out of bounds")
|
||||
}
|
||||
|
||||
// If this is not the first edit, (applied backwards), check if
|
||||
// this edit overlaps the previous one (and is therefore a developer error)
|
||||
if idx != 0 {
|
||||
prevEdit := a[idx-1]
|
||||
if prevEdit.Location < edit.Location+oldLen {
|
||||
return "", fmt.Errorf("2 edits overlap")
|
||||
}
|
||||
}
|
||||
|
||||
s = s[:start] + edit.New + s[stop:]
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func StripComments(sql string) (string, []string, error) {
|
||||
s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(sql)))
|
||||
var lines, comments []string
|
||||
for s.Scan() {
|
||||
t := s.Text()
|
||||
if strings.HasPrefix(t, "-- name:") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(t, "/* name:") && strings.HasSuffix(t, "*/") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(t, "# name:") {
|
||||
continue
|
||||
}
|
||||
if after, ok := strings.CutPrefix(t, "--"); ok {
|
||||
comments = append(comments, after)
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(t, "/*") && strings.HasSuffix(t, "*/") {
|
||||
// Preserve MySQL optimizer hints, which share block-comment
|
||||
// syntax but are semantically part of the query.
|
||||
if strings.HasPrefix(t, "/*+") {
|
||||
lines = append(lines, t)
|
||||
continue
|
||||
}
|
||||
t = strings.TrimPrefix(t, "/*")
|
||||
t = strings.TrimSuffix(t, "*/")
|
||||
comments = append(comments, t)
|
||||
continue
|
||||
}
|
||||
if after, ok := strings.CutPrefix(t, "#"); ok {
|
||||
comments = append(comments, after)
|
||||
continue
|
||||
}
|
||||
lines = append(lines, t)
|
||||
}
|
||||
return strings.Join(lines, "\n"), comments, s.Err()
|
||||
}
|
||||
|
||||
func CleanedComments(rawSQL string, cs CommentSyntax) ([]string, error) {
|
||||
s := bufio.NewScanner(strings.NewReader(strings.TrimSpace(rawSQL)))
|
||||
var comments []string
|
||||
for s.Scan() {
|
||||
line := s.Text()
|
||||
var prefix string
|
||||
if strings.HasPrefix(line, "--") {
|
||||
if !cs.Dash {
|
||||
continue
|
||||
}
|
||||
prefix = "--"
|
||||
}
|
||||
if strings.HasPrefix(line, "/*") {
|
||||
if !cs.SlashStar {
|
||||
continue
|
||||
}
|
||||
prefix = "/*"
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
if !cs.Hash {
|
||||
continue
|
||||
}
|
||||
prefix = "#"
|
||||
}
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
rest := line[len(prefix):]
|
||||
rest = strings.TrimSuffix(rest, "*/")
|
||||
comments = append(comments, rest)
|
||||
}
|
||||
return comments, s.Err()
|
||||
}
|
||||
210
internal/source/mutate_test.go
Normal file
210
internal/source/mutate_test.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newEdit is a testing helper for quickly generating Edits
|
||||
func newEdit(loc int, old, new string) Edit {
|
||||
return Edit{Location: loc, Old: old, New: new}
|
||||
}
|
||||
|
||||
// TestMutateSingle tests almost every possibility of a single edit
|
||||
func TestMutateSingle(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
edit Edit
|
||||
expected string
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
// Simple edits that replace everything
|
||||
{"", newEdit(0, "", ""), ""},
|
||||
{"a", newEdit(0, "a", "A"), "A"},
|
||||
{"abcde", newEdit(0, "abcde", "fghij"), "fghij"},
|
||||
{"", newEdit(0, "", "fghij"), "fghij"},
|
||||
{"abcde", newEdit(0, "abcde", ""), ""},
|
||||
|
||||
// Edits that start at the very beginning (But don't cover the whole range)
|
||||
{"abcde", newEdit(0, "a", "A"), "Abcde"},
|
||||
{"abcde", newEdit(0, "ab", "AB"), "ABcde"},
|
||||
{"abcde", newEdit(0, "abc", "ABC"), "ABCde"},
|
||||
{"abcde", newEdit(0, "abcd", "ABCD"), "ABCDe"},
|
||||
|
||||
// The above repeated, but with different lengths
|
||||
{"abcde", newEdit(0, "a", ""), "bcde"},
|
||||
{"abcde", newEdit(0, "ab", "A"), "Acde"},
|
||||
{"abcde", newEdit(0, "abc", "AB"), "ABde"},
|
||||
{"abcde", newEdit(0, "abcd", "AB"), "ABe"},
|
||||
|
||||
// Edits that touch the end (but don't cover the whole range)
|
||||
{"abcde", newEdit(4, "e", "E"), "abcdE"},
|
||||
{"abcde", newEdit(3, "de", "DE"), "abcDE"},
|
||||
{"abcde", newEdit(2, "cde", "CDE"), "abCDE"},
|
||||
{"abcde", newEdit(1, "bcde", "BCDE"), "aBCDE"},
|
||||
|
||||
// The above repeated, but with different lengths
|
||||
{"abcde", newEdit(4, "e", ""), "abcd"},
|
||||
{"abcde", newEdit(3, "de", "D"), "abcD"},
|
||||
{"abcde", newEdit(2, "cde", "CD"), "abCD"},
|
||||
{"abcde", newEdit(1, "bcde", "BC"), "aBC"},
|
||||
|
||||
// Raw insertions / deletions
|
||||
{"abcde", newEdit(0, "", "_"), "_abcde"},
|
||||
{"abcde", newEdit(1, "", "_"), "a_bcde"},
|
||||
{"abcde", newEdit(2, "", "_"), "ab_cde"},
|
||||
{"abcde", newEdit(3, "", "_"), "abc_de"},
|
||||
{"abcde", newEdit(4, "", "_"), "abcd_e"},
|
||||
{"abcde", newEdit(5, "", "_"), "abcde_"},
|
||||
}
|
||||
|
||||
origTests := tests
|
||||
// Generate the reverse mutations, for every edit - the opposite edit that makes it "undo"
|
||||
for _, spec := range origTests {
|
||||
tests = append(tests, test{
|
||||
input: spec.expected,
|
||||
edit: newEdit(spec.edit.Location, spec.edit.New, spec.edit.Old),
|
||||
expected: spec.input,
|
||||
})
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
expected := spec.expected
|
||||
|
||||
actual, err := Mutate(spec.input, []Edit{spec.edit})
|
||||
testName := fmt.Sprintf("Mutate(%s, Edit{%v, %v -> %v})", spec.input, spec.edit.Location, spec.edit.Old, spec.edit.New)
|
||||
if err != nil {
|
||||
t.Errorf("%s should not error (%v)", testName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf("%s expected %v; got %v", testName, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMutateMulti tests combinations of edits
|
||||
func TestMutateMulti(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
edit1 Edit
|
||||
edit2 Edit
|
||||
expected string
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
// Edits that are >1 character from each other
|
||||
{"abcde", newEdit(0, "a", "A"), newEdit(2, "c", "C"), "AbCde"},
|
||||
{"abcde", newEdit(0, "a", "A"), newEdit(2, "c", "C"), "AbCde"},
|
||||
|
||||
// 2 edits bump right up next to each other
|
||||
{"abcde", newEdit(0, "abc", ""), newEdit(3, "de", "DE"), "DE"},
|
||||
{"abcde", newEdit(0, "abc", "ABC"), newEdit(3, "de", ""), "ABC"},
|
||||
{"abcde", newEdit(0, "abc", "ABC"), newEdit(3, "de", "DE"), "ABCDE"},
|
||||
{"abcde", newEdit(1, "b", "BB"), newEdit(2, "c", "CC"), "aBBCCde"},
|
||||
|
||||
// 2 edits bump next to each other, but don't cover the whole string
|
||||
{"abcdef", newEdit(1, "bc", "C"), newEdit(3, "de", "D"), "aCDf"},
|
||||
{"abcde", newEdit(1, "bc", "CCCC"), newEdit(3, "d", "DDD"), "aCCCCDDDe"},
|
||||
|
||||
// lengthening edits
|
||||
{"abcde", newEdit(1, "b", "BBBB"), newEdit(2, "c", "CCCC"), "aBBBBCCCCde"},
|
||||
}
|
||||
|
||||
origTests := tests
|
||||
// Generate the edits in opposite order mutations, source edits should be independent of
|
||||
// the order the edits are specified
|
||||
for _, spec := range origTests {
|
||||
tests = append(tests, test{
|
||||
input: spec.input,
|
||||
edit1: spec.edit2,
|
||||
edit2: spec.edit1,
|
||||
expected: spec.expected,
|
||||
})
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
expected := spec.expected
|
||||
|
||||
actual, err := Mutate(spec.input, []Edit{spec.edit1, spec.edit2})
|
||||
testName := fmt.Sprintf("Mutate(%s, Edits{(%v, %v -> %v), (%v, %v -> %v)})", spec.input,
|
||||
spec.edit1.Location, spec.edit1.Old, spec.edit1.New,
|
||||
spec.edit2.Location, spec.edit2.Old, spec.edit2.New)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("%s should not error (%v)", testName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if actual != expected {
|
||||
t.Errorf("%s expected %v; got %v", testName, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMutateErrorSingle test errors are generated for trivially incorrect single edits
|
||||
func TestMutateErrorSingle(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
edit Edit
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
// old text is longer than input text
|
||||
{"", newEdit(0, "a", "A")},
|
||||
{"a", newEdit(0, "aa", "A")},
|
||||
{"hello", newEdit(0, "hello!", "A")},
|
||||
|
||||
// negative indexes
|
||||
{"aaa", newEdit(-1, "aa", "A")},
|
||||
{"aaa", newEdit(-2, "aa", "A")},
|
||||
{"aaa", newEdit(-100, "aa", "A")},
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
edit := spec.edit
|
||||
|
||||
_, err := Mutate(spec.input, []Edit{edit})
|
||||
testName := fmt.Sprintf("Mutate(%s, Edit{%v, %v -> %v})", spec.input, edit.Location, edit.Old, edit.New)
|
||||
if err == nil {
|
||||
t.Errorf("%s should error (%v)", testName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMutateErrorMulti tests error that can only happen across multiple errors
|
||||
func TestMutateErrorMulti(t *testing.T) {
|
||||
type test struct {
|
||||
input string
|
||||
edit1 Edit
|
||||
edit2 Edit
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
// These edits overlap each other, and are therefore undefined
|
||||
{"abcdef", newEdit(0, "a", ""), newEdit(0, "a", "A")},
|
||||
{"abcdef", newEdit(0, "ab", ""), newEdit(1, "ab", "AB")},
|
||||
{"abcdef", newEdit(0, "abc", ""), newEdit(2, "abc", "ABC")},
|
||||
|
||||
// the last edit is longer than the string itself
|
||||
{"abcdef", newEdit(0, "abcdefghi", ""), newEdit(2, "abc", "ABC")},
|
||||
|
||||
// negative indexes
|
||||
{"abcdef", newEdit(-1, "abc", ""), newEdit(3, "abc", "ABC")},
|
||||
{"abcdef", newEdit(0, "abc", ""), newEdit(-1, "abc", "ABC")},
|
||||
}
|
||||
|
||||
for _, spec := range tests {
|
||||
actual, err := Mutate(spec.input, []Edit{spec.edit1, spec.edit2})
|
||||
testName := fmt.Sprintf("Mutate(%s, Edits{(%v, %v -> %v), (%v, %v -> %v)})", spec.input,
|
||||
spec.edit1.Location, spec.edit1.Old, spec.edit1.New,
|
||||
spec.edit2.Location, spec.edit2.Old, spec.edit2.New)
|
||||
|
||||
if err == nil {
|
||||
t.Errorf("%s should error, but got (%v)", testName, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user