d2ir: Ensure filters pass before setting primary
This commit is contained in:
parent
df25d8b5fb
commit
88b885a753
8 changed files with 2774 additions and 26 deletions
|
|
@ -606,6 +606,15 @@ func (m *Map) IsFileMap() bool {
|
||||||
return m.Range.Start.Line == 0 && m.Range.Start.Column == 0
|
return m.Range.Start.Line == 0 && m.Range.Start.Column == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Map) HasFilter() bool {
|
||||||
|
for _, n := range m.Nodes {
|
||||||
|
if n.MapKey != nil && (n.MapKey.Ampersand || n.MapKey.NotAmpersand) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: require @ on import values for readability
|
// TODO: require @ on import values for readability
|
||||||
type Key struct {
|
type Key struct {
|
||||||
Range Range `json:"range"`
|
Range Range `json:"range"`
|
||||||
|
|
|
||||||
|
|
@ -376,6 +376,36 @@ func (g *globContext) prefixed(dst *Map) *globContext {
|
||||||
return &g2
|
return &g2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *compiler) ampersandFilterMap(dst *Map, ast, scopeAST *d2ast.Map) bool {
|
||||||
|
for _, n := range ast.Nodes {
|
||||||
|
switch {
|
||||||
|
case n.MapKey != nil:
|
||||||
|
ok := c.ampersandFilter(&RefContext{
|
||||||
|
Key: n.MapKey,
|
||||||
|
Scope: ast,
|
||||||
|
ScopeMap: dst,
|
||||||
|
ScopeAST: scopeAST,
|
||||||
|
})
|
||||||
|
if !ok {
|
||||||
|
// Unapply glob if appropriate.
|
||||||
|
gctx := c.getGlobContext(c.mapRefContextStack[len(c.mapRefContextStack)-1])
|
||||||
|
if gctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var ks string
|
||||||
|
if gctx.refctx.Key.HasTripleGlob() {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst)))
|
||||||
|
} else {
|
||||||
|
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
|
||||||
|
}
|
||||||
|
delete(gctx.appliedFields, ks)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
var globs []*globContext
|
var globs []*globContext
|
||||||
if len(c.globContextStack) > 0 {
|
if len(c.globContextStack) > 0 {
|
||||||
|
|
@ -400,32 +430,10 @@ func (c *compiler) compileMap(dst *Map, ast, scopeAST *d2ast.Map) {
|
||||||
c.globContextStack = c.globContextStack[:len(c.globContextStack)-1]
|
c.globContextStack = c.globContextStack[:len(c.globContextStack)-1]
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, n := range ast.Nodes {
|
ok := c.ampersandFilterMap(dst, ast, scopeAST)
|
||||||
switch {
|
|
||||||
case n.MapKey != nil:
|
|
||||||
ok := c.ampersandFilter(&RefContext{
|
|
||||||
Key: n.MapKey,
|
|
||||||
Scope: ast,
|
|
||||||
ScopeMap: dst,
|
|
||||||
ScopeAST: scopeAST,
|
|
||||||
})
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// Unapply glob if appropriate.
|
|
||||||
gctx := c.getGlobContext(c.mapRefContextStack[len(c.mapRefContextStack)-1])
|
|
||||||
if gctx == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ks string
|
|
||||||
if gctx.refctx.Key.HasTripleGlob() {
|
|
||||||
ks = d2format.Format(d2ast.MakeKeyPath(IDA(dst)))
|
|
||||||
} else {
|
|
||||||
ks = d2format.Format(d2ast.MakeKeyPath(BoardIDA(dst)))
|
|
||||||
}
|
|
||||||
delete(gctx.appliedFields, ks)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range ast.Nodes {
|
for _, n := range ast.Nodes {
|
||||||
switch {
|
switch {
|
||||||
|
|
@ -644,7 +652,22 @@ func (c *compiler) _ampersandFilter(f *Field, refctx *RefContext) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
if len(refctx.Key.Edges) == 0 && refctx.Key.Value.Null != nil {
|
// In case of filters, we need to pass filters before continuing
|
||||||
|
if refctx.Key.Value.Map != nil && refctx.Key.Value.Map.HasFilter() {
|
||||||
|
if f.Map() == nil {
|
||||||
|
f.Composite = &Map{
|
||||||
|
parent: f,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.mapRefContextStack = append(c.mapRefContextStack, refctx)
|
||||||
|
ok := c.ampersandFilterMap(f.Map(), refctx.Key.Value.Map, refctx.ScopeAST)
|
||||||
|
c.mapRefContextStack = c.mapRefContextStack[:len(c.mapRefContextStack)-1]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(refctx.Key.Edges) == 0 && (refctx.Key.Primary.Null != nil || refctx.Key.Value.Null != nil) {
|
||||||
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
|
// For vars, if we delete the field, it may just resolve to an outer scope var of the same name
|
||||||
// Instead we keep it around, so that resolveSubstitutions can find it
|
// Instead we keep it around, so that resolveSubstitutions can find it
|
||||||
if !IsVar(ParentMap(f)) {
|
if !IsVar(ParentMap(f)) {
|
||||||
|
|
@ -662,6 +685,7 @@ func (c *compiler) _compileField(f *Field, refctx *RefContext) {
|
||||||
Value: refctx.Key.Primary.Unbox(),
|
Value: refctx.Key.Primary.Unbox(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if refctx.Key.Value.Array != nil {
|
if refctx.Key.Value.Array != nil {
|
||||||
a := &Array{
|
a := &Array{
|
||||||
parent: f,
|
parent: f,
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,46 @@ b.label: a
|
||||||
assertQuery(t, m, 0, 0, "yellow", "b.style.fill")
|
assertQuery(t, m, 0, 0, "yellow", "b.style.fill")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "primary-filter",
|
||||||
|
run: func(t testing.TB) {
|
||||||
|
m, err := compile(t, `
|
||||||
|
parent: {
|
||||||
|
a -> b1
|
||||||
|
a -> b2
|
||||||
|
a -> b3
|
||||||
|
|
||||||
|
b1 -> c1
|
||||||
|
b1 -> c2
|
||||||
|
|
||||||
|
c1: {
|
||||||
|
c1-child.class: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
c2: {
|
||||||
|
C2-child.class: hidden
|
||||||
|
}
|
||||||
|
c2.class: hidden
|
||||||
|
b2.class: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
classes: {
|
||||||
|
hidden: {
|
||||||
|
style: {
|
||||||
|
fill: red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error
|
||||||
|
**: null {
|
||||||
|
&class: hidden
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
assert.Success(t, err)
|
||||||
|
assertQuery(t, m, 9, 3, nil, "")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
runa(t, tca)
|
runa(t, tca)
|
||||||
|
|
|
||||||
128
testdata/d2ir/TestCompile/filters/array.exp.json
generated
vendored
128
testdata/d2ir/TestCompile/filters/array.exp.json
generated
vendored
|
|
@ -114,6 +114,70 @@
|
||||||
"due_to_glob": false,
|
"due_to_glob": false,
|
||||||
"due_to_lazy_glob": false
|
"due_to_lazy_glob": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"edge": null,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:1:134-11:15:148",
|
||||||
|
"ampersand": true,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": {},
|
||||||
|
"value": {
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:9:142-11:15:148",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "server",
|
||||||
|
"raw_string": "server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"due_to_glob": true,
|
||||||
|
"due_to_lazy_glob": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"string": {
|
"string": {
|
||||||
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
|
@ -887,6 +951,70 @@
|
||||||
"due_to_glob": false,
|
"due_to_glob": false,
|
||||||
"due_to_lazy_glob": false
|
"due_to_lazy_glob": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"edge": null,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:1:134-11:15:148",
|
||||||
|
"ampersand": true,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "class",
|
||||||
|
"raw_string": "class"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": {},
|
||||||
|
"value": {
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/array.d2,11:9:142-11:15:148",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "server",
|
||||||
|
"raw_string": "server"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"due_to_glob": true,
|
||||||
|
"due_to_lazy_glob": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"string": {
|
"string": {
|
||||||
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
"range": "TestCompile/filters/array.d2,11:2:135-11:7:140",
|
||||||
|
|
|
||||||
64
testdata/d2ir/TestCompile/filters/base.exp.json
generated
vendored
64
testdata/d2ir/TestCompile/filters/base.exp.json
generated
vendored
|
|
@ -326,6 +326,70 @@
|
||||||
"due_to_glob": false,
|
"due_to_glob": false,
|
||||||
"due_to_lazy_glob": false
|
"due_to_lazy_glob": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"string": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"edge": null,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:1:62-7:18:79",
|
||||||
|
"ampersand": true,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": {},
|
||||||
|
"value": {
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/base.d2,7:9:70-7:18:79",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "rectangle",
|
||||||
|
"raw_string": "rectangle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"due_to_glob": true,
|
||||||
|
"due_to_lazy_glob": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"string": {
|
"string": {
|
||||||
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
"range": "TestCompile/filters/base.d2,7:2:63-7:7:68",
|
||||||
|
|
|
||||||
64
testdata/d2ir/TestCompile/filters/lazy-filter.exp.json
generated
vendored
64
testdata/d2ir/TestCompile/filters/lazy-filter.exp.json
generated
vendored
|
|
@ -362,6 +362,70 @@
|
||||||
"due_to_glob": false,
|
"due_to_glob": false,
|
||||||
"due_to_lazy_glob": false
|
"due_to_lazy_glob": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"string": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "label",
|
||||||
|
"raw_string": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "label",
|
||||||
|
"raw_string": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"edge": null,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:2:8-2:11:17",
|
||||||
|
"ampersand": true,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "label",
|
||||||
|
"raw_string": "label"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": {},
|
||||||
|
"value": {
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/lazy-filter.d2,2:10:16-2:11:17",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "a",
|
||||||
|
"raw_string": "a"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"due_to_glob": true,
|
||||||
|
"due_to_lazy_glob": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"string": {
|
"string": {
|
||||||
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
"range": "TestCompile/filters/lazy-filter.d2,2:3:9-2:8:14",
|
||||||
|
|
|
||||||
64
testdata/d2ir/TestCompile/filters/order.exp.json
generated
vendored
64
testdata/d2ir/TestCompile/filters/order.exp.json
generated
vendored
|
|
@ -326,6 +326,70 @@
|
||||||
"due_to_glob": false,
|
"due_to_glob": false,
|
||||||
"due_to_lazy_glob": false
|
"due_to_lazy_glob": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"string": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"key_path": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"edge": null,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:1:86-8:18:103",
|
||||||
|
"ampersand": true,
|
||||||
|
"key": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
"path": [
|
||||||
|
{
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "shape",
|
||||||
|
"raw_string": "shape"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"primary": {},
|
||||||
|
"value": {
|
||||||
|
"unquoted_string": {
|
||||||
|
"range": "TestCompile/filters/order.d2,8:9:94-8:18:103",
|
||||||
|
"value": [
|
||||||
|
{
|
||||||
|
"string": "rectangle",
|
||||||
|
"raw_string": "rectangle"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"due_to_glob": true,
|
||||||
|
"due_to_lazy_glob": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"string": {
|
"string": {
|
||||||
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
"range": "TestCompile/filters/order.d2,8:2:87-8:7:92",
|
||||||
|
|
|
||||||
2355
testdata/d2ir/TestCompile/filters/primary-filter.exp.json
generated
vendored
Normal file
2355
testdata/d2ir/TestCompile/filters/primary-filter.exp.json
generated
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue