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:
172
internal/metadata/meta.go
Normal file
172
internal/metadata/meta.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/sqlc-dev/sqlc/internal/constants"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/sqlc-dev/sqlc/internal/source"
|
||||
)
|
||||
|
||||
type CommentSyntax source.CommentSyntax
|
||||
|
||||
type Metadata struct {
|
||||
Name string
|
||||
Cmd string
|
||||
Comments []string
|
||||
Params map[string]string
|
||||
Flags map[string]bool
|
||||
|
||||
// RuleSkiplist contains the names of rules to disable vetting for.
|
||||
// If the map is empty, but the disable vet flag is specified, then all rules are ignored.
|
||||
RuleSkiplist map[string]struct{}
|
||||
|
||||
Filename string
|
||||
}
|
||||
|
||||
const (
|
||||
CmdExec = ":exec"
|
||||
CmdExecResult = ":execresult"
|
||||
CmdExecRows = ":execrows"
|
||||
CmdExecLastId = ":execlastid"
|
||||
CmdMany = ":many"
|
||||
CmdOne = ":one"
|
||||
CmdCopyFrom = ":copyfrom"
|
||||
CmdBatchExec = ":batchexec"
|
||||
CmdBatchMany = ":batchmany"
|
||||
CmdBatchOne = ":batchone"
|
||||
)
|
||||
|
||||
// A query name must be a valid Go identifier
|
||||
//
|
||||
// https://golang.org/ref/spec#Identifiers
|
||||
func validateQueryName(name string) error {
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("invalid query name: %q", name)
|
||||
}
|
||||
for i, c := range name {
|
||||
isLetter := unicode.IsLetter(c) || c == '_'
|
||||
isDigit := unicode.IsDigit(c)
|
||||
if i == 0 && !isLetter {
|
||||
return fmt.Errorf("invalid query name %q", name)
|
||||
} else if !(isLetter || isDigit) {
|
||||
return fmt.Errorf("invalid query name %q", name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseQueryNameAndType(t string, commentStyle CommentSyntax) (string, string, error) {
|
||||
for line := range strings.SplitSeq(t, "\n") {
|
||||
var prefix string
|
||||
if strings.HasPrefix(line, "--") {
|
||||
if !commentStyle.Dash {
|
||||
continue
|
||||
}
|
||||
prefix = "--"
|
||||
}
|
||||
if strings.HasPrefix(line, "/*") {
|
||||
if !commentStyle.SlashStar {
|
||||
continue
|
||||
}
|
||||
prefix = "/*"
|
||||
}
|
||||
if strings.HasPrefix(line, "#") {
|
||||
if !commentStyle.Hash {
|
||||
continue
|
||||
}
|
||||
prefix = "#"
|
||||
}
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
rest := line[len(prefix):]
|
||||
if !strings.HasPrefix(strings.TrimSpace(rest), "name") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(rest, ":") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(rest, " name: ") {
|
||||
return "", "", fmt.Errorf("invalid metadata: %s", line)
|
||||
}
|
||||
|
||||
part := strings.Split(strings.TrimSpace(line), " ")
|
||||
if prefix == "/*" {
|
||||
part = part[:len(part)-1] // removes the trailing "*/" element
|
||||
}
|
||||
if len(part) == 3 {
|
||||
return "", "", fmt.Errorf("missing query type [':one', ':many', ':exec', ':execrows', ':execlastid', ':execresult', ':copyfrom', 'batchexec', 'batchmany', 'batchone']: %s", line)
|
||||
}
|
||||
if len(part) != 4 {
|
||||
return "", "", fmt.Errorf("invalid query comment: %s", line)
|
||||
}
|
||||
queryName := part[2]
|
||||
queryType := strings.TrimSpace(part[3])
|
||||
switch queryType {
|
||||
case CmdOne, CmdMany, CmdExec, CmdExecResult, CmdExecRows, CmdExecLastId, CmdCopyFrom, CmdBatchExec, CmdBatchMany, CmdBatchOne:
|
||||
default:
|
||||
return "", "", fmt.Errorf("invalid query type: %s", queryType)
|
||||
}
|
||||
if err := validateQueryName(queryName); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return queryName, queryType, nil
|
||||
}
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
// ParseCommentFlags processes the comments provided with queries to determine the metadata params, flags and rules to skip.
|
||||
// All flags in query comments are prefixed with `@`, e.g. @param, @@sqlc-vet-disable.
|
||||
func ParseCommentFlags(comments []string) (map[string]string, map[string]bool, map[string]struct{}, error) {
|
||||
params := make(map[string]string)
|
||||
flags := make(map[string]bool)
|
||||
ruleSkiplist := make(map[string]struct{})
|
||||
|
||||
for _, line := range comments {
|
||||
s := bufio.NewScanner(strings.NewReader(line))
|
||||
s.Split(bufio.ScanWords)
|
||||
|
||||
s.Scan()
|
||||
token := s.Text()
|
||||
|
||||
if !strings.HasPrefix(token, "@") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch token {
|
||||
case constants.QueryFlagParam:
|
||||
s.Scan()
|
||||
name := s.Text()
|
||||
var rest []string
|
||||
for s.Scan() {
|
||||
paramToken := s.Text()
|
||||
rest = append(rest, paramToken)
|
||||
}
|
||||
params[name] = strings.Join(rest, " ")
|
||||
|
||||
case constants.QueryFlagSqlcVetDisable:
|
||||
flags[token] = true
|
||||
|
||||
// Vet rules can all be disabled in the same line or split across lines .i.e.
|
||||
// /* @sqlc-vet-disable sqlc/db-prepare delete-without-where */
|
||||
// is equivalent to:
|
||||
// /* @sqlc-vet-disable sqlc/db-prepare */
|
||||
// /* @sqlc-vet-disable delete-without-where */
|
||||
for s.Scan() {
|
||||
ruleSkiplist[s.Text()] = struct{}{}
|
||||
}
|
||||
|
||||
default:
|
||||
flags[token] = true
|
||||
}
|
||||
|
||||
if s.Err() != nil {
|
||||
return params, flags, ruleSkiplist, s.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return params, flags, ruleSkiplist, nil
|
||||
}
|
||||
182
internal/metadata/meta_test.go
Normal file
182
internal/metadata/meta_test.go
Normal file
@@ -0,0 +1,182 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseQueryNameAndType(t *testing.T) {
|
||||
|
||||
for _, query := range []string{
|
||||
`-- name: CreateFoo, :one`,
|
||||
`-- name: 9Foo_, :one`,
|
||||
`-- name: CreateFoo :two`,
|
||||
`-- name: CreateFoo`,
|
||||
`-- name: CreateFoo :one something`,
|
||||
`-- name: `,
|
||||
`--name: CreateFoo :one`,
|
||||
`--name CreateFoo :one`,
|
||||
`--name: CreateFoo :two`,
|
||||
"-- name:CreateFoo",
|
||||
`--name:CreateFoo :two`,
|
||||
} {
|
||||
if _, _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err == nil {
|
||||
t.Errorf("expected invalid metadata: %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
for _, query := range []string{
|
||||
`-- some comment`,
|
||||
`-- name comment`,
|
||||
`--name comment`,
|
||||
} {
|
||||
if _, _, err := ParseQueryNameAndType(query, CommentSyntax{Dash: true}); err != nil {
|
||||
t.Errorf("expected valid comment: %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
for query, cs := range map[string]CommentSyntax{
|
||||
`-- name: CreateFoo :one`: {Dash: true},
|
||||
`# name: CreateFoo :one`: {Hash: true},
|
||||
`/* name: CreateFoo :one */`: {SlashStar: true},
|
||||
} {
|
||||
queryName, queryCmd, err := ParseQueryNameAndType(query, cs)
|
||||
if err != nil {
|
||||
t.Errorf("expected valid metadata: %q", query)
|
||||
}
|
||||
if queryName != "CreateFoo" {
|
||||
t.Errorf("incorrect queryName parsed: (%q) %q", queryName, query)
|
||||
}
|
||||
if queryCmd != CmdOne {
|
||||
t.Errorf("incorrect queryCmd parsed: (%q) %q", queryCmd, query)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestParseQueryParams(t *testing.T) {
|
||||
for _, comments := range [][]string{
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @param foo_id UUID",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one ",
|
||||
" @param foo_id UUID ",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
"@param foo_id UUID",
|
||||
" invalid",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @invalid",
|
||||
" @param foo_id UUID",
|
||||
},
|
||||
{
|
||||
" name: GetFoos :many ",
|
||||
" @param foo_id UUID ",
|
||||
" @param @invalid UUID ",
|
||||
},
|
||||
} {
|
||||
params, _, _, err := ParseCommentFlags(comments)
|
||||
if err != nil {
|
||||
t.Errorf("expected comments to parse, got err: %s", err)
|
||||
}
|
||||
|
||||
pt, ok := params["foo_id"]
|
||||
if !ok {
|
||||
t.Errorf("expected param not found")
|
||||
}
|
||||
|
||||
if pt != "UUID" {
|
||||
t.Error("unexpected param metadata:", pt)
|
||||
}
|
||||
|
||||
_, ok = params["invalid"]
|
||||
if ok {
|
||||
t.Errorf("unexpected param found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseQueryFlags(t *testing.T) {
|
||||
for _, comments := range [][]string{
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @flag-foo",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one ",
|
||||
"@flag-foo ",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @flag-foo @flag-bar",
|
||||
},
|
||||
{
|
||||
" name: GetFoos :many",
|
||||
" @param @flag-bar UUID",
|
||||
" @flag-foo",
|
||||
},
|
||||
{
|
||||
" name: GetFoos :many",
|
||||
" @flag-foo",
|
||||
" @param @flag-bar UUID",
|
||||
},
|
||||
} {
|
||||
_, flags, _, err := ParseCommentFlags(comments)
|
||||
if err != nil {
|
||||
t.Errorf("expected comments to parse, got err: %s", err)
|
||||
}
|
||||
|
||||
if !flags["@flag-foo"] {
|
||||
t.Errorf("expected flag not found")
|
||||
}
|
||||
|
||||
if flags["@flag-bar"] {
|
||||
t.Errorf("unexpected flag found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseQueryRuleSkiplist(t *testing.T) {
|
||||
for _, comments := range [][]string{
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @sqlc-vet-disable sqlc/db-prepare delete-without-where ",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one ",
|
||||
" @sqlc-vet-disable sqlc/db-prepare ",
|
||||
" @sqlc-vet-disable delete-without-where ",
|
||||
},
|
||||
{
|
||||
" name: CreateFoo :one",
|
||||
" @sqlc-vet-disable sqlc/db-prepare ",
|
||||
" update-without where",
|
||||
" @sqlc-vet-disable delete-without-where ",
|
||||
},
|
||||
} {
|
||||
_, flags, ruleSkiplist, err := ParseCommentFlags(comments)
|
||||
if err != nil {
|
||||
t.Errorf("expected comments to parse, got err: %s", err)
|
||||
}
|
||||
|
||||
if !flags["@sqlc-vet-disable"] {
|
||||
t.Errorf("expected @sqlc-vet-disable flag not found")
|
||||
}
|
||||
|
||||
if _, ok := ruleSkiplist["sqlc/db-prepare"]; !ok {
|
||||
t.Errorf("expected rule not found in skiplist")
|
||||
}
|
||||
|
||||
if _, ok := ruleSkiplist["delete-without-where"]; !ok {
|
||||
t.Errorf("expected rule not found in skiplist")
|
||||
}
|
||||
|
||||
if _, ok := ruleSkiplist["update-without-where"]; ok {
|
||||
t.Errorf("unexpected rule found in skiplist")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user