Compare commits

..

3 Commits

Author SHA1 Message Date
Zakhar Bessarab
bb54075c23 docs/victoriametrics/changelog: cut v1.119.0
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 16:48:02 +04:00
Zakhar Bessarab
08f5220bc3 app/{vmselect,vlselect}: run make vmui-update vmui-logs-update
Signed-off-by: Zakhar Bessarab <z.bessarab@victoriametrics.com>
2025-06-06 16:38:04 +04:00
DmitrySafonov
f9015da6eb app/promql/rollup_result_cache: include extra_filters in rollupCache key for multi-tenant support
This PR addresses two issues:

When tenant labels (e.g. vm_account_id, vm_project_id) are passed via
extra_filters, they were not included in the rollupCache key. This could
cause cache entries to be reused across different tenants, resulting in
incorrect query results.
If a tenant is specified only via extra_filters, and that tenant does
not exist in TenantsCached, it gets silently filtered out by
GetTenantTokensFromFilters, causing the query to fall back to a global
(non-tenant) query — which is likely unexpected and potentially unsafe.
This fix ensures correct tenant scoping and avoids unintended data
exposure or cache pollution.

Related issue
https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9001

---------

Co-authored-by: Zakhar Bessarab <me@zekker.dev>
Co-authored-by: Max Kotliar <kotlyar.maksim@gmail.com>
2025-06-06 15:56:36 +04:00
24 changed files with 359 additions and 2191 deletions

View File

@@ -528,6 +528,8 @@ vet:
check-all: fmt vet golangci-lint govulncheck
clean-checkers: remove-golangci-lint remove-govulncheck
test:
GOEXPERIMENT=synctest go test ./lib/... ./app/...
@@ -572,11 +574,12 @@ app-local-goos-goarch:
app-local-windows-goarch:
CGO_ENABLED=0 GOOS=windows GOARCH=$(GOARCH) go build $(RACE) -ldflags "$(GO_BUILDINFO)" -o bin/$(APP_NAME)-windows-$(GOARCH)$(RACE).exe $(PKG_PREFIX)/app/$(APP_NAME)
quicktemplate-gen:
go tool qtc
quicktemplate-gen: install-qtc
qtc
install-qtc:
which qtc || go install github.com/valyala/quicktemplate/qtc@latest
golangci-lint:
GOEXPERIMENT=synctest go tool golangci-lint run
golangci-lint: install-golangci-lint
GOEXPERIMENT=synctest golangci-lint run

File diff suppressed because one or more lines are too long

View File

@@ -35,10 +35,10 @@
<meta property="og:title" content="UI for VictoriaLogs">
<meta property="og:url" content="https://victoriametrics.com/products/victorialogs/">
<meta property="og:description" content="Explore your log data with VictoriaLogs UI">
<script type="module" crossorigin src="./assets/index-BaRvaPfA.js"></script>
<script type="module" crossorigin src="./assets/index-DhqzKCNf.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
<link rel="stylesheet" crossorigin href="./assets/index-D5re9hC6.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -818,6 +818,7 @@ func QueryHandler(qt *querytracer.Tracer, startTime time.Time, w http.ResponseWr
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},
@@ -927,6 +928,7 @@ func queryRangeHandler(qt *querytracer.Tracer, startTime time.Time, w http.Respo
LookbackDelta: lookbackDelta,
RoundDigits: getRoundDigits(r),
EnforcedTagFilterss: etfs,
CacheTagFilters: etfs,
GetRequestURI: func() string {
return httpserver.GetRequestURI(r)
},

View File

@@ -140,6 +140,13 @@ type EvalConfig struct {
// EnforcedTagFilterss may contain additional label filters to use in the query.
EnforcedTagFilterss [][]storage.TagFilter
// CacheTagFilters stores the original tag-filter sets and extra_label from the request.
// The slice is never modified after creation and is used only to build
// the query-cache key.
//
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9001
CacheTagFilters [][]storage.TagFilter
// The callback, which returns the request URI during logging.
// The request URI isn't stored here because its' construction may take non-trivial amounts of CPU.
GetRequestURI func() string
@@ -166,6 +173,7 @@ func copyEvalConfig(src *EvalConfig) *EvalConfig {
ec.LookbackDelta = src.LookbackDelta
ec.RoundDigits = src.RoundDigits
ec.EnforcedTagFilterss = src.EnforcedTagFilterss
ec.CacheTagFilters = src.CacheTagFilters
ec.GetRequestURI = src.GetRequestURI
ec.QueryStats = src.QueryStats

View File

@@ -291,7 +291,7 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
bb := bbPool.Get()
defer bbPool.Put(bb)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
metainfoBuf := rrc.c.Get(nil, bb.B)
if len(metainfoBuf) == 0 {
qt.Printf("nothing found")
@@ -313,7 +313,7 @@ func (rrc *rollupResultCache) GetSeries(qt *querytracer.Tracer, ec *EvalConfig,
if !ok {
mi.RemoveKey(key)
metainfoBuf = mi.Marshal(metainfoBuf[:0])
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
bb.B = marshalRollupResultCacheKeyForSeries(bb.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
rrc.c.Set(bb.B, metainfoBuf)
return nil, ec.Start
}
@@ -419,7 +419,7 @@ func (rrc *rollupResultCache) PutSeries(qt *querytracer.Tracer, ec *EvalConfig,
metainfoBuf := bbPool.Get()
defer bbPool.Put(metainfoBuf)
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], expr, window, ec.Step, ec.EnforcedTagFilterss)
metainfoKey.B = marshalRollupResultCacheKeyForSeries(metainfoKey.B[:0], expr, window, ec.Step, ec.CacheTagFilters)
metainfoBuf.B = rrc.c.Get(metainfoBuf.B[:0], metainfoKey.B)
var mi rollupResultCacheMetainfo
if len(metainfoBuf.B) > 0 {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -36,10 +36,10 @@
<meta property="og:title" content="UI for VictoriaMetrics">
<meta property="og:url" content="https://victoriametrics.com/">
<meta property="og:description" content="Explore and troubleshoot your VictoriaMetrics data">
<script type="module" crossorigin src="./assets/index-xmjGcv4-.js"></script>
<script type="module" crossorigin src="./assets/index-D-ssBbZq.js"></script>
<link rel="modulepreload" crossorigin href="./assets/vendor-D8IJGiEn.js">
<link rel="stylesheet" crossorigin href="./assets/vendor-D1GxaB_c.css">
<link rel="stylesheet" crossorigin href="./assets/index-C85_NB5q.css">
<link rel="stylesheet" crossorigin href="./assets/index-D5re9hC6.css">
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -0,0 +1,78 @@
package tests
import (
"os"
"testing"
"github.com/VictoriaMetrics/VictoriaMetrics/apptest"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)
func TestClusterRollupResultCache(t *testing.T) {
os.RemoveAll(t.Name())
cmpOpt := cmpopts.IgnoreFields(apptest.PrometheusAPIV1QueryResponse{}, "Status", "Data.ResultType")
tc := apptest.NewTestCase(t)
defer tc.Stop()
vmstorage := tc.MustStartVmstorage("vmstorage", []string{
"-storageDataPath=" + tc.Dir() + "/vmstorage",
"-retentionPeriod=100y",
})
vminsert := tc.MustStartVminsert("vminsert", []string{
"-storageNode=" + vmstorage.VminsertAddr(),
})
vmselect := tc.MustStartVmselect("vmselect", []string{
"-storageNode=" + vmstorage.VmselectAddr(),
"-search.tenantCacheExpireDuration=0",
})
var tenantLabelsSamples = []string{
`foo_bar{vm_account_id="5"} 1.00 1652169720000`, // 2022-05-10T08:00:00Z'
`foo_bar{vm_account_id="5",vm_project_id="15"} 3.00 1652169720000`, // 2022-05-10T08:02:00Z
}
vminsert.PrometheusAPIV1ImportPrometheus(t, tenantLabelsSamples, apptest.QueryOpts{Tenant: "multitenant"})
vmstorage.ForceFlush(t)
want := apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[
{"metric":{"__name__":"foo_bar","vm_account_id":"5","vm_project_id": "0"},"values":[[1652169720,"1"],[1652169780,"1"]]},
{"metric":{"__name__":"foo_bar","vm_account_id":"5","vm_project_id":"15"},"values":[[1652169720,"3"],[1652169780,"3"]]}
]
}
}`,
)
got := vmselect.PrometheusAPIV1QueryRange(t, `foo_bar{}`, apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T07:59:00.000Z",
End: "2022-05-10T08:05:00.000Z",
Step: "1m",
ExtraFilters: []string{`{vm_account_id="5",vm_project_id="15"}`, `{vm_account_id="5",vm_project_id="0"}`},
})
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
want = apptest.NewPrometheusAPIV1QueryResponse(t,
`{"data":
{"result":[]}
}`,
)
got = vmselect.PrometheusAPIV1QueryRange(t, `foo_bar{}`, apptest.QueryOpts{
Tenant: "multitenant",
Start: "2022-05-10T07:59:00.000Z",
End: "2022-05-10T08:05:00.000Z",
Step: "1m",
ExtraFilters: []string{`{vm_account_id="99",vm_project_id="99"}`},
})
if diff := cmp.Diff(want, got, cmpOpt); diff != "" {
t.Errorf("unexpected response (-want, +got):\n%s", diff)
}
}

View File

@@ -18,6 +18,10 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
## tip
## [v1.119.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.119.0)
Released at 2025-06-06
* FEATURE: improve performance on systems with many CPU cores by removing the top sources of [false sharing](https://en.wikipedia.org/wiki/False_sharing) at global variables. See [#8682](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8682). Thanks to @tIGO for raising this issue and for the initial attempt to fix it at the [PR #8683](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8683).
* FEATURE: [vmgateway](https://docs.victoriametrics.com/victoriametrics/vmgateway/): add an option to use mTLS for connections to `-write.url` and `-read.url`. See [#8841](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/8841).
* FEATURE: [vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): dynamically adjusts the concurrent dial limit between 8 and 64 based on `-search.maxConcurrentRequests`. Additionally, goroutines now have the opportunity to access available connections while awaiting the dial limit token. This enables faster connection establishment when sudden requests arrive and reduces the blocking time during the availability check for connections. See [#8922](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/8922)
@@ -38,6 +42,7 @@ See also [LTS releases](https://docs.victoriametrics.com/victoriametrics/lts-rel
* BUGFIX: [vmctl](https://docs.victoriametrics.com/victoriametrics/vmctl/): enable dual-stack network mode for `vmctl`connections by default. This allows connecting to IPv6 endpoints as it was not possible previously. See [#9116](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9116).
* BUGFIX: [vmalert-tool](https://docs.victoriametrics.com/victoriametrics/vmalert-tool/): fix access conflicts for the temporary test folder when multiple users run tests on the same host. Thanks to @evkuzin for the [PR 9015](https://github.com/VictoriaMetrics/VictoriaMetrics/pull/9015).
* BUGFIX: [alerts](https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/deployment/docker/rules): fix the alerting rule `ScrapePoolHasNoTargets`. Previously, it may cause false positive in [sharding mode](https://docs.victoriametrics.com/victoriametrics/vmagent/#scraping-big-number-of-targets).
* BUGFIX: [vmselect](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/) in [VictoriaMetrics cluster](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/): include full list of query filters to build a cache key for [multitenant read](https://docs.victoriametrics.com/victoriametrics/cluster-victoriametrics/#multitenancy-via-labels) queries. Previously, cache key did not include tenancy related labels so cache entries could be shared between tenants breaking tenant isolation. See [#9002](https://github.com/VictoriaMetrics/VictoriaMetrics/issues/9002) for details.
## [v1.118.0](https://github.com/VictoriaMetrics/VictoriaMetrics/releases/tag/v1.118.0)

4
go.mod
View File

@@ -135,9 +135,7 @@ require (
github.com/prometheus/sigv4 v0.2.0 // indirect
github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
@@ -182,5 +180,3 @@ require (
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)
tool github.com/valyala/quicktemplate/qtc

8
go.sum
View File

@@ -332,14 +332,14 @@ github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 h1:4+LP7qmsLSGbmc66m1s5dKRMBwztRppfxFKlYqYte/c=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32/go.mod h1:kzh+BSAvpoyHHdHBCDhmSWtBc1NbLMZ2lWHqnBoxFks=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@@ -1,205 +0,0 @@
package parser
import (
"fmt"
"go/ast"
goparser "go/parser"
"strings"
)
type funcType struct {
name string
defPrefix string
callPrefix string
argNames string
args string
}
func parseFuncDef(b []byte) (*funcType, error) {
defStr := string(b)
// extract func name
n := strings.Index(defStr, "(")
if n < 0 {
return nil, fmt.Errorf("cannot find '(' in function definition")
}
name := defStr[:n]
defStr = defStr[n+1:]
defPrefix := ""
callPrefix := ""
if len(name) == 0 {
// Either empty func name or valid method definition. Let's check.
// parse method receiver
n = strings.Index(defStr, ")")
if n < 0 {
return nil, fmt.Errorf("cannot find ')' in func")
}
recvStr := defStr[:n]
defStr = defStr[n+1:]
exprStr := fmt.Sprintf("func (%s)", recvStr)
expr, err := goparser.ParseExpr(exprStr)
if err != nil {
return nil, fmt.Errorf("invalid method definition: %s", err)
}
ft := expr.(*ast.FuncType)
if len(ft.Params.List) != 1 || len(ft.Params.List[0].Names) != 1 {
// method receiver must contain only one param
return nil, fmt.Errorf("missing func or method name")
}
recvName := ft.Params.List[0].Names[0].Name
defPrefix = fmt.Sprintf("(%s) ", recvStr)
callPrefix = recvName + "."
// extract method name
n = strings.Index(defStr, "(")
if n < 0 {
return nil, fmt.Errorf("missing func name")
}
name = string(stripLeadingSpace([]byte(defStr[:n])))
if len(name) == 0 {
return nil, fmt.Errorf("missing method name")
}
defStr = defStr[n+1:]
}
// validate and collect func args
if len(defStr) == 0 || defStr[len(defStr)-1] != ')' {
return nil, fmt.Errorf("missing ')' at the end of func")
}
args := defStr[:len(defStr)-1]
exprStr := fmt.Sprintf("func (%s)", args)
expr, err := goparser.ParseExpr(exprStr)
if err != nil {
return nil, fmt.Errorf("invalid func args: %s", err)
}
ft := expr.(*ast.FuncType)
if ft.Results != nil {
return nil, fmt.Errorf("func mustn't return any results")
}
// extract arg names
var tmp []string
for _, f := range ft.Params.List {
if len(f.Names) == 0 {
return nil, fmt.Errorf("func cannot contain untyped arguments")
}
for _, n := range f.Names {
if n == nil {
return nil, fmt.Errorf("func cannot contain untyped arguments")
}
if _, isVariadic := f.Type.(*ast.Ellipsis); isVariadic {
tmp = append(tmp, n.Name+"...")
} else {
tmp = append(tmp, n.Name)
}
}
}
argNames := strings.Join(tmp, ", ")
if len(args) > 0 {
args = ", " + args
}
if len(argNames) > 0 {
argNames = ", " + argNames
}
return &funcType{
name: name,
defPrefix: defPrefix,
callPrefix: callPrefix,
argNames: argNames,
args: args,
}, nil
}
func parseFuncCall(b []byte) (*funcType, error) {
exprStr := string(b)
expr, err := goparser.ParseExpr(exprStr)
if err != nil {
return nil, err
}
ce, ok := expr.(*ast.CallExpr)
if !ok {
return nil, fmt.Errorf("missing function call")
}
callPrefix, name, err := getCallName(ce)
if err != nil {
return nil, err
}
argNames := exprStr[ce.Lparen : ce.Rparen-1]
if len(argNames) > 0 {
argNames = ", " + argNames
}
return &funcType{
name: name,
callPrefix: callPrefix,
argNames: argNames,
}, nil
}
func (f *funcType) DefStream(dst string) string {
return fmt.Sprintf("%s%s%s(%s *qt%s.Writer%s)", f.defPrefix, f.prefixStream(), f.name, dst, mangleSuffix, f.args)
}
func (f *funcType) CallStream(dst string) string {
return fmt.Sprintf("%s%s%s(%s%s)", f.callPrefix, f.prefixStream(), f.name, dst, f.argNames)
}
func (f *funcType) DefWrite(dst string) string {
return fmt.Sprintf("%s%s%s(%s qtio%s.Writer%s)", f.defPrefix, f.prefixWrite(), f.name, dst, mangleSuffix, f.args)
}
func (f *funcType) CallWrite(dst string) string {
return fmt.Sprintf("%s%s%s(%s%s)", f.callPrefix, f.prefixWrite(), f.name, dst, f.argNames)
}
func (f *funcType) DefString() string {
args := f.args
if len(args) > 0 {
// skip the first ', '
args = args[2:]
}
return fmt.Sprintf("%s%s(%s) string", f.defPrefix, f.name, args)
}
func (f *funcType) prefixWrite() string {
s := "write"
if isUpper(f.name[0]) {
s = "Write"
}
return s
}
func (f *funcType) prefixStream() string {
s := "stream"
if isUpper(f.name[0]) {
s = "Stream"
}
return s
}
func getCallName(ce *ast.CallExpr) (string, string, error) {
callPrefix := ""
name := ""
expr := ce.Fun
for {
switch x := expr.(type) {
case *ast.Ident:
if len(callPrefix) == 0 && len(name) == 0 {
return "", x.Name, nil
}
callPrefix = x.Name + "." + callPrefix
return callPrefix, name, nil
case *ast.SelectorExpr:
if len(name) == 0 {
name = x.Sel.Name
} else {
callPrefix = x.Sel.Name + "." + callPrefix
}
expr = x.X
default:
return "", "", fmt.Errorf("unexpected function name")
}
}
}

View File

@@ -1,921 +0,0 @@
package parser
import (
"bytes"
"fmt"
"go/ast"
goparser "go/parser"
gotoken "go/token"
"io"
"path/filepath"
"strconv"
"strings"
)
type parser struct {
s *scanner
w io.Writer
packageName string
skipLineComments bool
prefix string
forDepth int
switchDepth int
skipOutputDepth int
importsUseEmitted bool
packageNameEmitted bool
}
// Parse parses the contents of the supplied reader, writing generated code to
// the supplied writer. Uses filename as the source file for line comments, and
// pkg as the Go package name.
func Parse(w io.Writer, r io.Reader, filename, pkg string) error {
return parse(w, r, filename, pkg, false)
}
// ParseNoLineComments is the same as Parse, but does not write line comments.
func ParseNoLineComments(w io.Writer, r io.Reader, filename, pkg string) error {
return parse(w, r, filename, pkg, true)
}
func parse(w io.Writer, r io.Reader, filename, pkg string, skipLineComments bool) error {
p := &parser{
s: newScanner(r, filename),
w: w,
packageName: pkg,
skipLineComments: skipLineComments,
}
return p.parseTemplate()
}
func (p *parser) parseTemplate() error {
s := p.s
fmt.Fprintf(p.w, `// Code generated by qtc from %q. DO NOT EDIT.
// See https://github.com/valyala/quicktemplate for details.
`,
filepath.Base(s.filePath))
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitComment(t.Value)
case tagName:
switch string(t.Value) {
case "package":
if p.packageNameEmitted {
return fmt.Errorf("package name must be at the top of the template. Found at %s", s.Context())
}
if err := p.parsePackageName(); err != nil {
return err
}
case "import":
p.emitPackageName()
if p.importsUseEmitted {
return fmt.Errorf("imports must be at the top of the template. Found at %s", s.Context())
}
if err := p.parseImport(); err != nil {
return err
}
default:
p.emitPackageName()
p.emitImportsUse()
switch string(t.Value) {
case "interface", "iface":
if err := p.parseInterface(); err != nil {
return err
}
case "code":
if err := p.parseTemplateCode(); err != nil {
return err
}
case "func":
if err := p.parseFunc(); err != nil {
return err
}
default:
return fmt.Errorf("unexpected tag found outside func: %q at %s", t.Value, s.Context())
}
}
default:
return fmt.Errorf("unexpected token found %s outside func at %s", t, s.Context())
}
}
p.emitImportsUse()
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse template: %s", err)
}
return nil
}
func (p *parser) emitPackageName() {
if !p.packageNameEmitted {
p.Printf("package %s\n", p.packageName)
p.packageNameEmitted = true
}
}
func (p *parser) emitComment(comment []byte) {
isFirstNonemptyLine := false
for len(comment) > 0 {
n := bytes.IndexByte(comment, '\n')
if n < 0 {
n = len(comment)
}
line := stripTrailingSpace(comment[:n])
if bytes.HasPrefix(line, []byte("//")) {
line = line[2:]
if len(line) > 0 && isSpace(line[0]) {
line = line[1:]
}
}
if len(line) == 0 {
if isFirstNonemptyLine {
fmt.Fprintf(p.w, "//\n")
}
} else {
fmt.Fprintf(p.w, "// %s\n", line)
isFirstNonemptyLine = true
}
if n < len(comment) {
comment = comment[n+1:]
} else {
comment = comment[n:]
}
}
fmt.Fprintf(p.w, "\n")
}
func (p *parser) emitImportsUse() {
if p.importsUseEmitted {
return
}
p.Printf(`import (
qtio%s "io"
qt%s "github.com/valyala/quicktemplate"
)
`, mangleSuffix, mangleSuffix)
p.Printf(`var (
_ = qtio%s.Copy
_ = qt%s.AcquireByteBuffer
)
`, mangleSuffix, mangleSuffix)
p.importsUseEmitted = true
}
func (p *parser) parseFunc() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
funcStr := "func " + string(t.Value)
f, err := parseFuncDef(t.Value)
if err != nil {
return fmt.Errorf("error in %q at %s: %s", funcStr, s.Context(), err)
}
p.emitFuncStart(f)
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitText(t.Value)
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error in %q: %s", funcStr, err)
}
if ok {
continue
}
switch string(t.Value) {
case "endfunc":
if err = skipTagContents(s); err != nil {
return err
}
p.emitFuncEnd(f)
return nil
default:
return fmt.Errorf("unexpected tag found in %q: %q at %s", funcStr, t.Value, s.Context())
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", funcStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", funcStr, err)
}
return fmt.Errorf("cannot find endfunc tag for %q at %s", funcStr, s.Context())
}
func (p *parser) parseFor() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
forStr := "for " + string(t.Value)
if err = validateForStmt(t.Value); err != nil {
return fmt.Errorf("invalid statement %q at %s: %s", forStr, s.Context(), err)
}
p.Printf("for %s {", t.Value)
p.prefix += "\t"
p.forDepth++
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitText(t.Value)
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error in %q: %s", forStr, err)
}
if ok {
continue
}
switch string(t.Value) {
case "endfor":
if err = skipTagContents(s); err != nil {
return err
}
p.forDepth--
p.prefix = p.prefix[1:]
p.Printf("}")
return nil
default:
return fmt.Errorf("unexpected tag found in %q: %q at %s", forStr, t.Value, s.Context())
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", forStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", forStr, err)
}
return fmt.Errorf("cannot find endfor tag for %q at %s", forStr, s.Context())
}
func (p *parser) parseDefault() error {
s := p.s
if err := skipTagContents(s); err != nil {
return err
}
stmtStr := "default"
p.Printf("default:")
p.prefix += "\t"
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitText(t.Value)
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error in %q: %s", stmtStr, err)
}
if !ok {
s.Rewind()
p.prefix = p.prefix[1:]
return nil
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", stmtStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", stmtStr, err)
}
return fmt.Errorf("cannot find end of %q at %s", stmtStr, s.Context())
}
func (p *parser) parseCase(switchValue string) error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
caseStr := "case " + string(t.Value)
if err = validateCaseStmt(switchValue, t.Value); err != nil {
return fmt.Errorf("invalid statement %q at %s: %s", caseStr, s.Context(), err)
}
p.Printf("case %s:", t.Value)
p.prefix += "\t"
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitText(t.Value)
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error in %q: %s", caseStr, err)
}
if !ok {
s.Rewind()
p.prefix = p.prefix[1:]
return nil
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", caseStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", caseStr, err)
}
return fmt.Errorf("cannot find end of %q at %s", caseStr, s.Context())
}
func (p *parser) parseCat() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
filename, err := strconv.Unquote(string(t.Value))
if err != nil {
return fmt.Errorf("invalid cat value %q at %s: %s", t.Value, s.Context(), err)
}
data, err := readFile(s.filePath, filename)
if err != nil {
return fmt.Errorf("cannot cat file %q at %s: %s", filename, s.Context(), err)
}
p.emitText(data)
return nil
}
func (p *parser) parseSwitch() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
switchStr := "switch " + string(t.Value)
if err = validateSwitchStmt(t.Value); err != nil {
return fmt.Errorf("invalid statement %q at %s: %s", switchStr, s.Context(), err)
}
p.Printf("switch %s {", t.Value)
switchValue := string(t.Value)
caseNum := 0
defaultFound := false
p.switchDepth++
for s.Next() {
t := s.Token()
switch t.ID {
case text:
if caseNum == 0 {
comment := stripLeadingSpace(t.Value)
if len(comment) > 0 {
p.emitComment(comment)
}
} else {
p.emitText(t.Value)
}
case tagName:
switch string(t.Value) {
case "endswitch":
if caseNum == 0 {
return fmt.Errorf("empty statement %q found at %s", switchStr, s.Context())
}
if err = skipTagContents(s); err != nil {
return err
}
p.switchDepth--
p.Printf("}")
return nil
case "case":
caseNum++
if err = p.parseCase(switchValue); err != nil {
return err
}
case "default":
if defaultFound {
return fmt.Errorf("duplicate default tag found in %q at %s", switchStr, s.Context())
}
defaultFound = true
caseNum++
if err = p.parseDefault(); err != nil {
return err
}
default:
return fmt.Errorf("unexpected tag found in %q: %q at %s", switchStr, t.Value, s.Context())
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", switchStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", switchStr, err)
}
return fmt.Errorf("cannot find endswitch tag for %q at %s", switchStr, s.Context())
}
func (p *parser) parseIf() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
if len(t.Value) == 0 {
return fmt.Errorf("empty if condition at %s", s.Context())
}
ifStr := "if " + string(t.Value)
if err = validateIfStmt(t.Value); err != nil {
return fmt.Errorf("invalid statement %q at %s: %s", ifStr, s.Context(), err)
}
p.Printf("if %s {", t.Value)
p.prefix += "\t"
elseUsed := false
for s.Next() {
t := s.Token()
switch t.ID {
case text:
p.emitText(t.Value)
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error in %q: %s", ifStr, err)
}
if ok {
continue
}
switch string(t.Value) {
case "endif":
if err = skipTagContents(s); err != nil {
return err
}
p.prefix = p.prefix[1:]
p.Printf("}")
return nil
case "else":
if elseUsed {
return fmt.Errorf("duplicate else branch found for %q at %s", ifStr, s.Context())
}
if err = skipTagContents(s); err != nil {
return err
}
p.prefix = p.prefix[1:]
p.Printf("} else {")
p.prefix += "\t"
elseUsed = true
case "elseif":
if elseUsed {
return fmt.Errorf("unexpected elseif branch found after else branch for %q at %s",
ifStr, s.Context())
}
t, err = expectTagContents(s)
if err != nil {
return err
}
p.prefix = p.prefix[1:]
p.Printf("} else if %s {", t.Value)
p.prefix += "\t"
default:
return fmt.Errorf("unexpected tag found in %q: %q at %s", ifStr, t.Value, s.Context())
}
default:
return fmt.Errorf("unexpected token found when parsing %q: %s at %s", ifStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse %q: %s", ifStr, err)
}
return fmt.Errorf("cannot find endif tag for %q at %s", ifStr, s.Context())
}
func (p *parser) tryParseCommonTags(tagBytes []byte) (bool, error) {
tagNameStr, prec := splitTagNamePrec(string(tagBytes))
switch tagNameStr {
case "s", "v", "d", "dl", "dul", "f", "q", "z", "j", "u",
"s=", "v=", "d=", "dl=", "dul=", "f=", "q=", "z=", "j=", "u=",
"sz", "qz", "jz", "uz",
"sz=", "qz=", "jz=", "uz=":
if err := p.parseOutputTag(tagNameStr, prec); err != nil {
return false, err
}
case "=", "=h", "=u", "=uh", "=q", "=qh", "=j", "=jh":
if err := p.parseOutputFunc(tagNameStr); err != nil {
return false, err
}
case "return":
if err := p.skipAfterTag(tagNameStr); err != nil {
return false, err
}
case "break":
if p.forDepth <= 0 && p.switchDepth <= 0 {
return false, fmt.Errorf("found break tag outside for loop and switch block")
}
if err := p.skipAfterTag(tagNameStr); err != nil {
return false, err
}
case "continue":
if p.forDepth <= 0 {
return false, fmt.Errorf("found continue tag outside for loop")
}
if err := p.skipAfterTag(tagNameStr); err != nil {
return false, err
}
case "code":
if err := p.parseFuncCode(); err != nil {
return false, err
}
case "for":
if err := p.parseFor(); err != nil {
return false, err
}
case "if":
if err := p.parseIf(); err != nil {
return false, err
}
case "switch":
if err := p.parseSwitch(); err != nil {
return false, err
}
case "cat":
if err := p.parseCat(); err != nil {
return false, err
}
default:
return false, nil
}
return true, nil
}
func splitTagNamePrec(tagName string) (string, int) {
parts := strings.Split(tagName, ".")
if len(parts) == 2 && parts[0] == "f" {
p := parts[1]
if strings.HasSuffix(p, "=") {
p = p[:len(p)-1]
}
if len(p) == 0 {
return "f", 0
}
prec, err := strconv.Atoi(p)
if err == nil && prec >= 0 {
return "f", prec
}
}
return tagName, -1
}
func (p *parser) skipAfterTag(tagStr string) error {
s := p.s
if err := skipTagContents(s); err != nil {
return err
}
p.Printf("%s", tagStr)
p.skipOutputDepth++
defer func() {
p.skipOutputDepth--
}()
for s.Next() {
t := s.Token()
switch t.ID {
case text:
// skip text
case tagName:
ok, err := p.tryParseCommonTags(t.Value)
if err != nil {
return fmt.Errorf("error when parsing contents after %q: %s", tagStr, err)
}
if ok {
continue
}
switch string(t.Value) {
case "endfunc", "endfor", "endif", "else", "elseif", "case", "default", "endswitch":
s.Rewind()
return nil
default:
return fmt.Errorf("unexpected tag found after %q: %q at %s", tagStr, t.Value, s.Context())
}
default:
return fmt.Errorf("unexpected token found when parsing contents after %q: %s at %s", tagStr, t, s.Context())
}
}
if err := s.LastError(); err != nil {
return fmt.Errorf("cannot parse contents after %q: %s", tagStr, err)
}
return fmt.Errorf("cannot find closing tag after %q at %s", tagStr, s.Context())
}
func (p *parser) parseInterface() error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
n := bytes.IndexByte(t.Value, '{')
if n < 0 {
return fmt.Errorf("missing '{' in interface at %s", s.Context())
}
ifname := string(stripTrailingSpace(t.Value[:n]))
if len(ifname) == 0 {
return fmt.Errorf("missing interface name at %s", s.Context())
}
p.Printf("type %s interface {", ifname)
p.prefix = "\t"
tail := t.Value[n:]
exprStr := fmt.Sprintf("interface %s", tail)
expr, err := goparser.ParseExpr(exprStr)
if err != nil {
return fmt.Errorf("error when parsing interface at %s: %s", s.Context(), err)
}
it, ok := expr.(*ast.InterfaceType)
if !ok {
return fmt.Errorf("unexpected interface type at %s: %T", s.Context(), expr)
}
methods := it.Methods.List
if len(methods) == 0 {
return fmt.Errorf("interface must contain at least one method at %s", s.Context())
}
for _, m := range it.Methods.List {
methodStr := exprStr[m.Pos()-1 : m.End()-1]
f, err := parseFuncDef([]byte(methodStr))
if err != nil {
return fmt.Errorf("when when parsing %q at %s: %s", methodStr, s.Context(), err)
}
p.Printf("%s string", methodStr)
p.Printf("%s", f.DefStream("qw"+mangleSuffix))
p.Printf("%s", f.DefWrite("qq"+mangleSuffix))
}
p.prefix = ""
p.Printf("}")
return nil
}
func (p *parser) parsePackageName() error {
t, err := expectTagContents(p.s)
if err != nil {
return err
}
if len(t.Value) == 0 {
return fmt.Errorf("empty package name found at %s", p.s.Context())
}
if err = validatePackageName(t.Value); err != nil {
return fmt.Errorf("invalid package name found at %s: %s", p.s.Context(), err)
}
p.packageName = string(t.Value)
p.emitPackageName()
return nil
}
func (p *parser) parseImport() error {
t, err := expectTagContents(p.s)
if err != nil {
return err
}
if len(t.Value) == 0 {
return fmt.Errorf("empty import found at %s", p.s.Context())
}
if err = validateImport(t.Value); err != nil {
return fmt.Errorf("invalid import found at %s: %s", p.s.Context(), err)
}
p.Printf("import %s\n", t.Value)
return nil
}
func (p *parser) parseTemplateCode() error {
t, err := expectTagContents(p.s)
if err != nil {
return err
}
if err = validateTemplateCode(t.Value); err != nil {
return fmt.Errorf("invalid code at %s: %s", p.s.Context(), err)
}
p.Printf("%s\n", t.Value)
return nil
}
func (p *parser) parseFuncCode() error {
t, err := expectTagContents(p.s)
if err != nil {
return err
}
if err = validateFuncCode(t.Value); err != nil {
return fmt.Errorf("invalid code at %s: %s", p.s.Context(), err)
}
p.Printf("%s\n", t.Value)
return nil
}
func (p *parser) parseOutputTag(tagNameStr string, prec int) error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
if err = validateOutputTagValue(t.Value); err != nil {
return fmt.Errorf("invalid output tag value at %s: %s", s.Context(), err)
}
filter := "N"
switch tagNameStr {
case "s", "v", "q", "z", "j", "sz", "qz", "jz":
filter = "E"
}
if strings.HasSuffix(tagNameStr, "=") {
tagNameStr = tagNameStr[:len(tagNameStr)-1]
}
if tagNameStr == "f" && prec >= 0 {
p.Printf("qw%s.N().FPrec(%s, %d)", mangleSuffix, t.Value, prec)
} else {
tagNameStr = strings.ToUpper(tagNameStr)
p.Printf("qw%s.%s().%s(%s)", mangleSuffix, filter, tagNameStr, t.Value)
}
return nil
}
func (p *parser) parseOutputFunc(tagNameStr string) error {
s := p.s
t, err := expectTagContents(s)
if err != nil {
return err
}
f, err := parseFuncCall(t.Value)
if err != nil {
return fmt.Errorf("error at %s: %s", s.Context(), err)
}
filter := "N"
tagNameStr = tagNameStr[1:]
if strings.HasSuffix(tagNameStr, "h") {
tagNameStr = tagNameStr[:len(tagNameStr)-1]
switch tagNameStr {
case "", "q", "j":
filter = "E"
}
}
if len(tagNameStr) > 0 || filter == "E" {
tagNameStr = strings.ToUpper(tagNameStr)
p.Printf("{")
p.Printf("qb%s := qt%s.AcquireByteBuffer()", mangleSuffix, mangleSuffix)
p.Printf("%s", f.CallWrite("qb"+mangleSuffix))
p.Printf("qw%s.%s().%sZ(qb%s.B)", mangleSuffix, filter, tagNameStr, mangleSuffix)
p.Printf("qt%s.ReleaseByteBuffer(qb%s)", mangleSuffix, mangleSuffix)
p.Printf("}")
} else {
p.Printf("%s", f.CallStream("qw"+mangleSuffix))
}
return nil
}
func (p *parser) emitText(text []byte) {
for len(text) > 0 {
n := bytes.IndexByte(text, '`')
if n < 0 {
p.Printf("qw%s.N().S(`%s`)", mangleSuffix, text)
return
}
p.Printf("qw%s.N().S(`%s`)", mangleSuffix, text[:n])
p.Printf("qw%s.N().S(\"`\")", mangleSuffix)
text = text[n+1:]
}
}
func (p *parser) emitFuncStart(f *funcType) {
p.Printf("func %s {", f.DefStream("qw"+mangleSuffix))
p.prefix = "\t"
}
func (p *parser) emitFuncEnd(f *funcType) {
p.prefix = ""
p.Printf("}\n")
p.Printf("func %s {", f.DefWrite("qq"+mangleSuffix))
p.prefix = "\t"
p.Printf("qw%s := qt%s.AcquireWriter(qq%s)", mangleSuffix, mangleSuffix, mangleSuffix)
p.Printf("%s", f.CallStream("qw"+mangleSuffix))
p.Printf("qt%s.ReleaseWriter(qw%s)", mangleSuffix, mangleSuffix)
p.prefix = ""
p.Printf("}\n")
p.Printf("func %s {", f.DefString())
p.prefix = "\t"
p.Printf("qb%s := qt%s.AcquireByteBuffer()", mangleSuffix, mangleSuffix)
p.Printf("%s", f.CallWrite("qb"+mangleSuffix))
p.Printf("qs%s := string(qb%s.B)", mangleSuffix, mangleSuffix)
p.Printf("qt%s.ReleaseByteBuffer(qb%s)", mangleSuffix, mangleSuffix)
p.Printf("return qs%s", mangleSuffix)
p.prefix = ""
p.Printf("}\n")
}
func (p *parser) Printf(format string, args ...interface{}) {
if p.skipOutputDepth > 0 {
return
}
w := p.w
if !p.skipLineComments {
// line comments are required to start at the beginning of the line
p.s.WriteLineComment(w)
}
fmt.Fprintf(w, "%s", p.prefix)
fmt.Fprintf(w, format, args...)
fmt.Fprintf(w, "\n")
}
func skipTagContents(s *scanner) error {
tagName := string(s.Token().Value)
t, err := expectTagContents(s)
if err != nil {
return err
}
if len(t.Value) > 0 {
return fmt.Errorf("unexpected extra value after %s: %q at %s", tagName, t.Value, s.Context())
}
return err
}
func expectTagContents(s *scanner) (*token, error) {
return expectToken(s, tagContents)
}
func expectToken(s *scanner, id int) (*token, error) {
if !s.Next() {
return nil, fmt.Errorf("cannot find token %s: %v", tokenIDToStr(id), s.LastError())
}
t := s.Token()
if t.ID != id {
return nil, fmt.Errorf("unexpected token found %s. Expecting %s at %s", t, tokenIDToStr(id), s.Context())
}
return t, nil
}
func validateOutputTagValue(stmt []byte) error {
exprStr := string(stmt)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateForStmt(stmt []byte) error {
exprStr := fmt.Sprintf("func () { for %s {} }", stmt)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateIfStmt(stmt []byte) error {
exprStr := fmt.Sprintf("func () { if %s {} }", stmt)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateSwitchStmt(stmt []byte) error {
exprStr := fmt.Sprintf("func () { switch %s {} }", stmt)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateCaseStmt(switchValue string, stmt []byte) error {
exprStr := fmt.Sprintf("func () { switch %s {case %s:} }", switchValue, stmt)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateFuncCode(code []byte) error {
exprStr := fmt.Sprintf("func () { for { %s\n } }", code)
_, err := goparser.ParseExpr(exprStr)
return err
}
func validateTemplateCode(code []byte) error {
codeStr := fmt.Sprintf("package foo\nvar _ = a\n%s", code)
fset := gotoken.NewFileSet()
_, err := goparser.ParseFile(fset, "", codeStr, 0)
return err
}
func validatePackageName(code []byte) error {
codeStr := fmt.Sprintf("package %s", code)
fset := gotoken.NewFileSet()
_, err := goparser.ParseFile(fset, "", codeStr, 0)
return err
}
func validateImport(code []byte) error {
codeStr := fmt.Sprintf("package foo\nimport %s", code)
fset := gotoken.NewFileSet()
f, err := goparser.ParseFile(fset, "", codeStr, 0)
if err != nil {
return err
}
for _, d := range f.Decls {
gd, ok := d.(*ast.GenDecl)
if !ok {
return fmt.Errorf("unexpected code found: %T. Expecting ast.GenDecl", d)
}
for _, s := range gd.Specs {
if _, ok := s.(*ast.ImportSpec); !ok {
return fmt.Errorf("unexpected code found: %T. Expecting ast.ImportSpec", s)
}
}
}
return nil
}

View File

@@ -1,467 +0,0 @@
package parser
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strings"
)
// token ids
const (
text = iota
tagName
tagContents
)
var tokenStrMap = map[int]string{
text: "text",
tagName: "tagName",
tagContents: "tagContents",
}
func tokenIDToStr(id int) string {
str := tokenStrMap[id]
if str == "" {
panic(fmt.Sprintf("unknown tokenID=%d", id))
}
return str
}
type token struct {
ID int
Value []byte
line int
pos int
}
func (t *token) init(id, line, pos int) {
t.ID = id
t.Value = t.Value[:0]
t.line = line
t.pos = pos
}
func (t *token) String() string {
return fmt.Sprintf("Token %q, value %q", tokenIDToStr(t.ID), t.Value)
}
type scanner struct {
r *bufio.Reader
t token
c byte
err error
filePath string
line int
lineStr []byte
nextTokenID int
capture bool
capturedValue []byte
collapseSpaceDepth int
stripSpaceDepth int
stripToNewLine bool
rewind bool
}
var tailOfLine = regexp.MustCompile(`^[[:blank:]]*(?:\r*\n)?`)
var prevBlank = regexp.MustCompile(`[[:blank:]]+$`)
func newScanner(r io.Reader, filePath string) *scanner {
// Substitute backslashes with forward slashes in filePath
// for the sake of consistency on different platforms (windows, linux).
// See https://github.com/valyala/quicktemplate/issues/62.
filePath = strings.Replace(filePath, "\\", "/", -1)
return &scanner{
r: bufio.NewReader(r),
filePath: filePath,
}
}
func (s *scanner) Rewind() {
if s.rewind {
panic("BUG: duplicate Rewind call")
}
s.rewind = true
}
func (s *scanner) Next() bool {
if s.rewind {
s.rewind = false
return true
}
for {
if !s.scanToken() {
return false
}
switch s.t.ID {
case text:
if s.stripToNewLine {
s.t.Value = tailOfLine.ReplaceAll(s.t.Value, nil)
s.stripToNewLine = false
}
if len(s.t.Value) == 0 {
// skip empty text
continue
}
case tagName:
switch string(s.t.Value) {
case "comment":
if !s.skipComment() {
return false
}
continue
case "plain":
if !s.readPlain() {
return false
}
if len(s.t.Value) == 0 {
// skip empty text
continue
}
case "collapsespace":
if !s.readTagContents() {
return false
}
s.collapseSpaceDepth++
continue
case "stripspace":
if !s.readTagContents() {
return false
}
s.stripSpaceDepth++
continue
case "endcollapsespace":
if s.collapseSpaceDepth == 0 {
s.err = fmt.Errorf("endcollapsespace tag found without the corresponding collapsespace tag")
return false
}
if !s.readTagContents() {
return false
}
s.collapseSpaceDepth--
continue
case "endstripspace":
if s.stripSpaceDepth == 0 {
s.err = fmt.Errorf("endstripspace tag found without the corresponding stripspace tag")
return false
}
if !s.readTagContents() {
return false
}
s.stripSpaceDepth--
continue
case "space":
if !s.readTagContents() {
return false
}
s.t.init(text, s.t.line, s.t.pos)
s.t.Value = append(s.t.Value[:0], ' ')
return true
case "newline":
if !s.readTagContents() {
return false
}
s.t.init(text, s.t.line, s.t.pos)
s.t.Value = append(s.t.Value[:0], '\n')
return true
}
}
return true
}
}
func (s *scanner) readPlain() bool {
if !s.readTagContents() {
return false
}
startLine := s.line
startPos := s.pos()
s.startCapture()
ok := s.skipUntilTag("endplain")
v := s.stopCapture()
s.t.init(text, startLine, startPos)
if ok {
n := bytes.LastIndex(v, strTagOpen)
v = v[:n]
s.t.Value = append(s.t.Value[:0], v...)
}
return ok
}
var strTagOpen = []byte("{%")
func (s *scanner) skipComment() bool {
if !s.readTagContents() {
return false
}
return s.skipUntilTag("endcomment")
}
func (s *scanner) skipUntilTag(tagName string) bool {
ok := false
for {
if !s.nextByte() {
break
}
if s.c != '{' {
continue
}
if !s.nextByte() {
break
}
if s.c != '%' {
s.unreadByte('~')
continue
}
ok = s.readTagName()
s.nextTokenID = text
if !ok {
s.err = nil
continue
}
if string(s.t.Value) == tagName {
ok = s.readTagContents()
break
}
}
if !ok {
s.err = fmt.Errorf("cannot find %q tag: %s", tagName, s.err)
}
return ok
}
func (s *scanner) scanToken() bool {
switch s.nextTokenID {
case text:
return s.readText()
case tagName:
return s.readTagName()
case tagContents:
return s.readTagContents()
default:
panic(fmt.Sprintf("BUG: unknown nextTokenID %d", s.nextTokenID))
}
}
func (s *scanner) readText() bool {
s.t.init(text, s.line, s.pos())
ok := false
for {
if !s.nextByte() {
ok = (len(s.t.Value) > 0)
break
}
if s.c != '{' {
s.appendByte()
continue
}
if !s.nextByte() {
s.appendByte()
ok = true
break
}
if s.c == '%' {
s.nextTokenID = tagName
ok = true
if !s.nextByte() {
s.appendByte()
break
}
if s.c != '-' {
s.unreadByte(s.c)
break
}
s.t.Value = prevBlank.ReplaceAll(s.t.Value, nil)
break
}
s.unreadByte('{')
s.appendByte()
}
if s.stripSpaceDepth > 0 {
s.t.Value = stripSpace(s.t.Value)
} else if s.collapseSpaceDepth > 0 {
s.t.Value = collapseSpace(s.t.Value)
}
return ok
}
func (s *scanner) readTagName() bool {
s.skipSpace()
s.t.init(tagName, s.line, s.pos())
for {
if s.isSpace() || s.c == '%' {
if s.c == '%' {
s.unreadByte('~')
}
s.nextTokenID = tagContents
return true
}
if (s.c >= 'a' && s.c <= 'z') || (s.c >= 'A' && s.c <= 'Z') || (s.c >= '0' && s.c <= '9') || s.c == '=' || s.c == '.' {
s.appendByte()
if !s.nextByte() {
return false
}
continue
}
if s.c == '-' {
s.unreadByte(s.c)
s.nextTokenID = tagContents
return true
}
s.err = fmt.Errorf("unexpected character: '%c'", s.c)
s.unreadByte('~')
return false
}
}
func (s *scanner) readTagContents() bool {
s.skipSpace()
s.t.init(tagContents, s.line, s.pos())
for {
if s.c != '%' {
s.appendByte()
if !s.nextByte() {
return false
}
continue
}
if !s.nextByte() {
s.appendByte()
return false
}
if s.c == '}' {
if bytes.HasSuffix(s.t.Value, []byte("-")) {
s.t.Value = s.t.Value[:len(s.t.Value)-1]
s.stripToNewLine = true
}
s.nextTokenID = text
s.t.Value = stripTrailingSpace(s.t.Value)
return true
}
s.unreadByte('%')
s.appendByte()
if !s.nextByte() {
return false
}
}
}
func (s *scanner) skipSpace() {
for s.nextByte() && s.isSpace() {
}
}
func (s *scanner) isSpace() bool {
return isSpace(s.c)
}
func (s *scanner) nextByte() bool {
if s.err != nil {
return false
}
c, err := s.r.ReadByte()
if err != nil {
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
s.err = err
return false
}
if c == '\n' {
s.line++
s.lineStr = s.lineStr[:0]
} else {
s.lineStr = append(s.lineStr, c)
}
s.c = c
if s.capture {
s.capturedValue = append(s.capturedValue, c)
}
return true
}
func (s *scanner) startCapture() {
s.capture = true
s.capturedValue = s.capturedValue[:0]
}
func (s *scanner) stopCapture() []byte {
s.capture = false
v := s.capturedValue
s.capturedValue = s.capturedValue[:0]
return v
}
func (s *scanner) Token() *token {
return &s.t
}
func (s *scanner) LastError() error {
if s.err == nil {
return nil
}
if s.err == io.ErrUnexpectedEOF && s.t.ID == text {
if s.collapseSpaceDepth > 0 {
return fmt.Errorf("missing endcollapsespace tag at %s", s.Context())
}
if s.stripSpaceDepth > 0 {
return fmt.Errorf("missing endstripspace tag at %s", s.Context())
}
return nil
}
return fmt.Errorf("error when reading %s at %s: %s",
tokenIDToStr(s.t.ID), s.Context(), s.err)
}
func (s *scanner) appendByte() {
s.t.Value = append(s.t.Value, s.c)
}
func (s *scanner) unreadByte(c byte) {
if err := s.r.UnreadByte(); err != nil {
panic(fmt.Sprintf("BUG: bufio.Reader.UnreadByte returned non-nil error: %s", err))
}
if s.capture {
s.capturedValue = s.capturedValue[:len(s.capturedValue)-1]
}
if s.c == '\n' {
s.line--
s.lineStr = s.lineStr[:0] // TODO: use correct line
} else {
s.lineStr = s.lineStr[:len(s.lineStr)-1]
}
s.c = c
}
func (s *scanner) pos() int {
return len(s.lineStr)
}
func (s *scanner) Context() string {
t := s.Token()
return fmt.Sprintf("file %q, line %d, pos %d, token %s, last line %s",
s.filePath, t.line+1, t.pos, snippet(t.Value), snippet(s.lineStr))
}
func (s *scanner) WriteLineComment(w io.Writer) {
fmt.Fprintf(w, "//line %s:%d\n", s.filePath, s.t.line+1)
}
func snippet(s []byte) string {
if len(s) <= 40 {
return fmt.Sprintf("%q", s)
}
return fmt.Sprintf("%q ... %q", s[:20], s[len(s)-20:])
}

View File

@@ -1,96 +0,0 @@
package parser
import (
"bytes"
"errors"
"io/ioutil"
"path/filepath"
"unicode"
)
// mangleSuffix is used for mangling quicktemplate-specific names
// in the generated code, so they don't clash with user-provided names.
const mangleSuffix = "422016"
func stripLeadingSpace(b []byte) []byte {
for len(b) > 0 && isSpace(b[0]) {
b = b[1:]
}
return b
}
func stripTrailingSpace(b []byte) []byte {
for len(b) > 0 && isSpace(b[len(b)-1]) {
b = b[:len(b)-1]
}
return b
}
func collapseSpace(b []byte) []byte {
return stripSpaceExt(b, true)
}
func stripSpace(b []byte) []byte {
return stripSpaceExt(b, false)
}
func stripSpaceExt(b []byte, isCollapse bool) []byte {
if len(b) == 0 {
return b
}
var dst []byte
if isCollapse && isSpace(b[0]) {
dst = append(dst, ' ')
}
isLastSpace := isSpace(b[len(b)-1])
for len(b) > 0 {
n := bytes.IndexByte(b, '\n')
if n < 0 {
n = len(b)
}
z := b[:n]
if n == len(b) {
b = b[n:]
} else {
b = b[n+1:]
}
z = stripLeadingSpace(z)
z = stripTrailingSpace(z)
if len(z) == 0 {
continue
}
dst = append(dst, z...)
if isCollapse {
dst = append(dst, ' ')
}
}
if isCollapse && !isLastSpace && len(dst) > 0 {
dst = dst[:len(dst)-1]
}
return dst
}
func isSpace(c byte) bool {
return unicode.IsSpace(rune(c))
}
func isUpper(c byte) bool {
return unicode.IsUpper(rune(c))
}
func readFile(cwd, filename string) ([]byte, error) {
if len(filename) == 0 {
return nil, errors.New("filename cannot be empty")
}
if filename[0] != '/' {
cwdAbs, err := filepath.Abs(cwd)
if err != nil {
return nil, err
}
dir, _ := filepath.Split(cwdAbs)
filename = filepath.Join(dir, filename)
}
return ioutil.ReadFile(filename)
}

View File

@@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Aliaksandr Valialkin, VertaMedia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,33 +0,0 @@
# qtc
Template compiler (converter) for [quicktemplate](https://github.com/valyala/quicktemplate).
Converts quicktemplate files into Go code. By default these files
have `.qtpl` extension.
# Usage
```
$ go get -u github.com/valyala/quicktemplate/qtc
$ qtc -h
```
`qtc` may be called either directly or via [go generate](https://blog.golang.org/generate).
The latter case is preffered. Just put the following line near the `main` function:
```go
package main
//go:generate qtc -dir=path/to/directory/with/templates
func main() {
// main code here
}
```
Then run `go generate` whenever you need re-generating template code.
Directory with templates may contain arbirary number of subdirectories -
`qtc` generates template code recursively for each subdirectory.
Directories with templates may also contain arbitrary `.go` files - contents
of these files may be used inside templates. Such Go files usually contain
various helper functions and structs.

View File

@@ -1,174 +0,0 @@
// Command qtc is a compiler for quicktemplate files.
//
// See https://github.com/valyala/quicktemplate/qtc for details.
package main
import (
"flag"
"go/format"
"io/ioutil"
"log"
"os"
"path/filepath"
"sort"
"strings"
"github.com/valyala/quicktemplate/parser"
)
var (
dir = flag.String("dir", ".", "Path to directory with template files to compile. "+
"Only files with ext extension are compiled. See ext flag for details.\n"+
"The compiler recursively processes all the subdirectories.\n"+
"Compiled template files are placed near the original file with .go extension added.")
file = flag.String("file", "", "Path to template file to compile.\n"+
"Flags -dir and -ext are ignored if file is set.\n"+
"The compiled file will be placed near the original file with .go extension added.")
ext = flag.String("ext", "qtpl", "Only files with this extension are compiled")
skipLineComments = flag.Bool("skipLineComments", false, "Don't write line comments")
)
var logger = log.New(os.Stderr, "qtc: ", log.LstdFlags)
var filesCompiled int
func main() {
flag.Parse()
if len(*file) > 0 {
compileSingleFile(*file)
return
}
if len(*ext) == 0 {
logger.Fatalf("ext cannot be empty")
}
if len(*dir) == 0 {
*dir = "."
}
if (*ext)[0] != '.' {
*ext = "." + *ext
}
logger.Printf("Compiling *%s template files in directory %q", *ext, *dir)
compileDir(*dir)
logger.Printf("Total files compiled: %d", filesCompiled)
}
func compileSingleFile(filename string) {
fi, err := os.Stat(filename)
if err != nil {
logger.Fatalf("cannot stat file %q: %s", filename, err)
}
if fi.IsDir() {
logger.Fatalf("cannot compile directory %q. Use -dir flag", filename)
}
compileFile(filename)
}
func compileDir(path string) {
fi, err := os.Stat(path)
if err != nil {
logger.Fatalf("cannot compile files in %q: %s", path, err)
}
if !fi.IsDir() {
logger.Fatalf("cannot compile files in %q: it is not directory", path)
}
d, err := os.Open(path)
if err != nil {
logger.Fatalf("cannot compile files in %q: %s", path, err)
}
defer d.Close()
fis, err := d.Readdir(-1)
if err != nil {
logger.Fatalf("cannot read files in %q: %s", path, err)
}
var names []string
for _, fi = range fis {
name := fi.Name()
if name == "." || name == ".." {
continue
}
if !fi.IsDir() {
names = append(names, name)
} else {
subPath := filepath.Join(path, name)
compileDir(subPath)
}
}
sort.Strings(names)
for _, name := range names {
if strings.HasSuffix(name, *ext) {
filename := filepath.Join(path, name)
compileFile(filename)
}
}
}
func compileFile(infile string) {
outfile := infile + ".go"
logger.Printf("Compiling %q to %q...", infile, outfile)
inf, err := os.Open(infile)
if err != nil {
logger.Fatalf("cannot open file %q: %s", infile, err)
}
tmpfile := outfile + ".tmp"
outf, err := os.Create(tmpfile)
if err != nil {
logger.Fatalf("cannot create file %q: %s", tmpfile, err)
}
packageName, err := getPackageName(infile)
if err != nil {
logger.Fatalf("cannot determine package name for %q: %s", infile, err)
}
parseFunc := parser.Parse
if *skipLineComments {
parseFunc = parser.ParseNoLineComments
}
if err = parseFunc(outf, inf, infile, packageName); err != nil {
logger.Fatalf("error when parsing file %q: %s", infile, err)
}
if err = outf.Close(); err != nil {
logger.Fatalf("error when closing file %q: %s", tmpfile, err)
}
if err = inf.Close(); err != nil {
logger.Fatalf("error when closing file %q: %s", infile, err)
}
// prettify the output file
uglyCode, err := ioutil.ReadFile(tmpfile)
if err != nil {
logger.Fatalf("cannot read file %q: %s", tmpfile, err)
}
prettyCode, err := format.Source(uglyCode)
if err != nil {
logger.Fatalf("error when formatting compiled code for %q: %s. See %q for details", infile, err, tmpfile)
}
if err = ioutil.WriteFile(outfile, prettyCode, 0666); err != nil {
logger.Fatalf("error when writing file %q: %s", outfile, err)
}
if err = os.Remove(tmpfile); err != nil {
logger.Fatalf("error when removing file %q: %s", tmpfile, err)
}
filesCompiled++
}
func getPackageName(filename string) (string, error) {
filenameAbs, err := filepath.Abs(filename)
if err != nil {
return "", err
}
dir, _ := filepath.Split(filenameAbs)
return filepath.Base(dir), nil
}

6
vendor/modules.txt vendored
View File

@@ -560,13 +560,9 @@ github.com/puzpuzpuz/xsync/v3
# github.com/rivo/uniseg v0.4.7
## explicit; go 1.18
github.com/rivo/uniseg
# github.com/rogpeppe/go-internal v1.14.1
## explicit; go 1.23
# github.com/russross/blackfriday/v2 v2.1.0
## explicit
github.com/russross/blackfriday/v2
# github.com/spf13/pflag v1.0.6
## explicit; go 1.12
# github.com/stretchr/testify v1.10.0
## explicit; go 1.17
github.com/stretchr/testify/assert
@@ -597,8 +593,6 @@ github.com/valyala/histogram
# github.com/valyala/quicktemplate v1.8.0
## explicit; go 1.17
github.com/valyala/quicktemplate
github.com/valyala/quicktemplate/parser
github.com/valyala/quicktemplate/qtc
# github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1
## explicit; go 1.15
github.com/xrash/smetrics