mirror of
https://github.com/golang/go
synced 2024-10-02 08:28:36 -06:00
exp/template: fix bug in argument evaluation.
Must keep dot and the receiver separate - variables broke that symmetry. Also clean up function invocation and rename "data" to "dot" for clarity. R=golang-dev, dsymonds CC=golang-dev https://golang.org/cl/4678048
This commit is contained in:
parent
3c6a7b3fc5
commit
d366c36945
@ -106,32 +106,32 @@ func (t *Template) ExecuteInSet(wr io.Writer, data interface{}, set *Set) (err o
|
|||||||
|
|
||||||
// Walk functions step through the major pieces of the template structure,
|
// Walk functions step through the major pieces of the template structure,
|
||||||
// generating output as they go.
|
// generating output as they go.
|
||||||
func (s *state) walk(data reflect.Value, n node) {
|
func (s *state) walk(dot reflect.Value, n node) {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *actionNode:
|
case *actionNode:
|
||||||
s.line = n.line
|
s.line = n.line
|
||||||
defer s.pop(s.mark())
|
defer s.pop(s.mark())
|
||||||
s.printValue(n, s.evalPipeline(data, n.pipe))
|
s.printValue(n, s.evalPipeline(dot, n.pipe))
|
||||||
case *ifNode:
|
case *ifNode:
|
||||||
s.line = n.line
|
s.line = n.line
|
||||||
s.walkIfOrWith(nodeIf, data, n.pipe, n.list, n.elseList)
|
s.walkIfOrWith(nodeIf, dot, n.pipe, n.list, n.elseList)
|
||||||
case *listNode:
|
case *listNode:
|
||||||
for _, node := range n.nodes {
|
for _, node := range n.nodes {
|
||||||
s.walk(data, node)
|
s.walk(dot, node)
|
||||||
}
|
}
|
||||||
case *rangeNode:
|
case *rangeNode:
|
||||||
s.line = n.line
|
s.line = n.line
|
||||||
s.walkRange(data, n)
|
s.walkRange(dot, n)
|
||||||
case *templateNode:
|
case *templateNode:
|
||||||
s.line = n.line
|
s.line = n.line
|
||||||
s.walkTemplate(data, n)
|
s.walkTemplate(dot, n)
|
||||||
case *textNode:
|
case *textNode:
|
||||||
if _, err := s.wr.Write(n.text); err != nil {
|
if _, err := s.wr.Write(n.text); err != nil {
|
||||||
s.error(err)
|
s.error(err)
|
||||||
}
|
}
|
||||||
case *withNode:
|
case *withNode:
|
||||||
s.line = n.line
|
s.line = n.line
|
||||||
s.walkIfOrWith(nodeWith, data, n.pipe, n.list, n.elseList)
|
s.walkIfOrWith(nodeWith, dot, n.pipe, n.list, n.elseList)
|
||||||
default:
|
default:
|
||||||
s.errorf("unknown node: %s", n)
|
s.errorf("unknown node: %s", n)
|
||||||
}
|
}
|
||||||
@ -139,9 +139,9 @@ func (s *state) walk(data reflect.Value, n node) {
|
|||||||
|
|
||||||
// walkIfOrWith walks an 'if' or 'with' node. The two control structures
|
// walkIfOrWith walks an 'if' or 'with' node. The two control structures
|
||||||
// are identical in behavior except that 'with' sets dot.
|
// are identical in behavior except that 'with' sets dot.
|
||||||
func (s *state) walkIfOrWith(typ nodeType, data reflect.Value, pipe *pipeNode, list, elseList *listNode) {
|
func (s *state) walkIfOrWith(typ nodeType, dot reflect.Value, pipe *pipeNode, list, elseList *listNode) {
|
||||||
defer s.pop(s.mark())
|
defer s.pop(s.mark())
|
||||||
val := s.evalPipeline(data, pipe)
|
val := s.evalPipeline(dot, pipe)
|
||||||
truth, ok := isTrue(val)
|
truth, ok := isTrue(val)
|
||||||
if !ok {
|
if !ok {
|
||||||
s.errorf("if/with can't use value of type %T", val.Interface())
|
s.errorf("if/with can't use value of type %T", val.Interface())
|
||||||
@ -150,10 +150,10 @@ func (s *state) walkIfOrWith(typ nodeType, data reflect.Value, pipe *pipeNode, l
|
|||||||
if typ == nodeWith {
|
if typ == nodeWith {
|
||||||
s.walk(val, list)
|
s.walk(val, list)
|
||||||
} else {
|
} else {
|
||||||
s.walk(data, list)
|
s.walk(dot, list)
|
||||||
}
|
}
|
||||||
} else if elseList != nil {
|
} else if elseList != nil {
|
||||||
s.walk(data, elseList)
|
s.walk(dot, elseList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,9 +181,9 @@ func isTrue(val reflect.Value) (truth, ok bool) {
|
|||||||
return truth, true
|
return truth, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) walkRange(data reflect.Value, r *rangeNode) {
|
func (s *state) walkRange(dot reflect.Value, r *rangeNode) {
|
||||||
defer s.pop(s.mark())
|
defer s.pop(s.mark())
|
||||||
val, _ := indirect(s.evalPipeline(data, r.pipe))
|
val, _ := indirect(s.evalPipeline(dot, r.pipe))
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
case reflect.Array, reflect.Slice:
|
case reflect.Array, reflect.Slice:
|
||||||
if val.Len() == 0 {
|
if val.Len() == 0 {
|
||||||
@ -215,13 +215,13 @@ func (s *state) walkRange(data reflect.Value, r *rangeNode) {
|
|||||||
s.errorf("range can't iterate over value of type %T", val.Interface())
|
s.errorf("range can't iterate over value of type %T", val.Interface())
|
||||||
}
|
}
|
||||||
if r.elseList != nil {
|
if r.elseList != nil {
|
||||||
s.walk(data, r.elseList)
|
s.walk(dot, r.elseList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) walkTemplate(data reflect.Value, t *templateNode) {
|
func (s *state) walkTemplate(dot reflect.Value, t *templateNode) {
|
||||||
// Can't use evalArg because there are two types we expect.
|
// Can't use evalArg because there are two types we expect.
|
||||||
arg := s.evalEmptyInterface(data, t.name)
|
arg := s.evalEmptyInterface(dot, t.name)
|
||||||
if !arg.IsValid() {
|
if !arg.IsValid() {
|
||||||
s.errorf("invalid value in template invocation; expected string or *Template")
|
s.errorf("invalid value in template invocation; expected string or *Template")
|
||||||
}
|
}
|
||||||
@ -243,12 +243,12 @@ func (s *state) walkTemplate(data reflect.Value, t *templateNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
defer s.pop(s.mark())
|
defer s.pop(s.mark())
|
||||||
data = s.evalPipeline(data, t.pipe)
|
dot = s.evalPipeline(dot, t.pipe)
|
||||||
newState := *s
|
newState := *s
|
||||||
newState.tmpl = tmpl
|
newState.tmpl = tmpl
|
||||||
// No dynamic scoping: template invocations inherit no variables.
|
// No dynamic scoping: template invocations inherit no variables.
|
||||||
newState.vars = []variable{{"$", data}}
|
newState.vars = []variable{{"$", dot}}
|
||||||
newState.walk(data, tmpl.root)
|
newState.walk(dot, tmpl.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Eval functions evaluate pipelines, commands, and their elements and extract
|
// Eval functions evaluate pipelines, commands, and their elements and extract
|
||||||
@ -259,12 +259,12 @@ func (s *state) walkTemplate(data reflect.Value, t *templateNode) {
|
|||||||
// pipeline has a variable declaration, the variable will be pushed on the
|
// pipeline has a variable declaration, the variable will be pushed on the
|
||||||
// stack. Callers should therefore pop the stack after they are finished
|
// stack. Callers should therefore pop the stack after they are finished
|
||||||
// executing commands depending on the pipeline value.
|
// executing commands depending on the pipeline value.
|
||||||
func (s *state) evalPipeline(data reflect.Value, pipe *pipeNode) (value reflect.Value) {
|
func (s *state) evalPipeline(dot reflect.Value, pipe *pipeNode) (value reflect.Value) {
|
||||||
if pipe == nil {
|
if pipe == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, cmd := range pipe.cmds {
|
for _, cmd := range pipe.cmds {
|
||||||
value = s.evalCommand(data, cmd, value) // previous value is this one's final arg.
|
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
|
||||||
// If the object has type interface{}, dig down one level to the thing inside.
|
// If the object has type interface{}, dig down one level to the thing inside.
|
||||||
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
||||||
value = reflect.ValueOf(value.Interface()) // lovely!
|
value = reflect.ValueOf(value.Interface()) // lovely!
|
||||||
@ -282,23 +282,23 @@ func (s *state) notAFunction(args []node, final reflect.Value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.Value) reflect.Value {
|
func (s *state) evalCommand(dot reflect.Value, cmd *commandNode, final reflect.Value) reflect.Value {
|
||||||
firstWord := cmd.args[0]
|
firstWord := cmd.args[0]
|
||||||
switch n := firstWord.(type) {
|
switch n := firstWord.(type) {
|
||||||
case *fieldNode:
|
case *fieldNode:
|
||||||
return s.evalFieldNode(data, n, cmd.args, final)
|
return s.evalFieldNode(dot, n, cmd.args, final)
|
||||||
case *identifierNode:
|
case *identifierNode:
|
||||||
// Must be a function.
|
// Must be a function.
|
||||||
return s.evalFunction(data, n.ident, cmd.args, final)
|
return s.evalFunction(dot, n.ident, cmd.args, final)
|
||||||
case *variableNode:
|
case *variableNode:
|
||||||
return s.evalVariableNode(n, cmd.args, final)
|
return s.evalVariableNode(dot, n, cmd.args, final)
|
||||||
}
|
}
|
||||||
s.notAFunction(cmd.args, final)
|
s.notAFunction(cmd.args, final)
|
||||||
switch word := firstWord.(type) {
|
switch word := firstWord.(type) {
|
||||||
case *boolNode:
|
case *boolNode:
|
||||||
return reflect.ValueOf(word.true)
|
return reflect.ValueOf(word.true)
|
||||||
case *dotNode:
|
case *dotNode:
|
||||||
return data
|
return dot
|
||||||
case *numberNode:
|
case *numberNode:
|
||||||
// These are ideal constants but we don't know the type
|
// These are ideal constants but we don't know the type
|
||||||
// and we have no context. (If it was a method argument,
|
// and we have no context. (If it was a method argument,
|
||||||
@ -320,35 +320,35 @@ func (s *state) evalCommand(data reflect.Value, cmd *commandNode, final reflect.
|
|||||||
panic("not reached")
|
panic("not reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalFieldNode(data reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value {
|
func (s *state) evalFieldNode(dot reflect.Value, field *fieldNode, args []node, final reflect.Value) reflect.Value {
|
||||||
return s.evalFieldChain(data, field.ident, args, final)
|
return s.evalFieldChain(dot, dot, field.ident, args, final)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalVariableNode(v *variableNode, args []node, final reflect.Value) reflect.Value {
|
func (s *state) evalVariableNode(dot reflect.Value, v *variableNode, args []node, final reflect.Value) reflect.Value {
|
||||||
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
|
// $x.Field has $x as the first ident, Field as the second. Eval the var, then the fields.
|
||||||
data := s.varValue(v.ident[0])
|
value := s.varValue(v.ident[0])
|
||||||
if len(v.ident) == 1 {
|
if len(v.ident) == 1 {
|
||||||
return data
|
return value
|
||||||
}
|
}
|
||||||
return s.evalFieldChain(data, v.ident[1:], args, final)
|
return s.evalFieldChain(dot, value, v.ident[1:], args, final)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalFieldChain(data reflect.Value, ident []string, args []node, final reflect.Value) reflect.Value {
|
func (s *state) evalFieldChain(dot, receiver reflect.Value, ident []string, args []node, final reflect.Value) reflect.Value {
|
||||||
// Up to the last entry, it must be a field.
|
// Up to the last entry, it must be a field.
|
||||||
n := len(ident)
|
n := len(ident)
|
||||||
for i := 0; i < n-1; i++ {
|
for i := 0; i < n-1; i++ {
|
||||||
data = s.evalField(data, ident[i], nil, zero, false)
|
dot = s.evalField(dot, ident[i], nil, zero, zero)
|
||||||
}
|
}
|
||||||
// Now it can be a field or method and if a method, gets arguments.
|
// Now it can be a field or method and if a method, gets arguments.
|
||||||
return s.evalField(data, ident[n-1], args, final, true)
|
return s.evalField(dot, ident[n-1], args, final, receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalFunction(data reflect.Value, name string, args []node, final reflect.Value) reflect.Value {
|
func (s *state) evalFunction(dot reflect.Value, name string, args []node, final reflect.Value) reflect.Value {
|
||||||
function, ok := findFunction(name, s.tmpl, s.set)
|
function, ok := findFunction(name, s.tmpl, s.set)
|
||||||
if !ok {
|
if !ok {
|
||||||
s.errorf("%q is not a defined function", name)
|
s.errorf("%q is not a defined function", name)
|
||||||
}
|
}
|
||||||
return s.evalCall(data, function, name, false, args, final)
|
return s.evalCall(dot, zero, function, name, args, final)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this an exported - upper case - name?
|
// Is this an exported - upper case - name?
|
||||||
@ -362,25 +362,25 @@ func isExported(name string) bool {
|
|||||||
// value of the pipeline, if any.
|
// value of the pipeline, if any.
|
||||||
// If we're in a chain, such as (.X.Y.Z), .X and .Y cannot be methods;
|
// If we're in a chain, such as (.X.Y.Z), .X and .Y cannot be methods;
|
||||||
// canBeMethod will be true only for the last element of such chains (here .Z).
|
// canBeMethod will be true only for the last element of such chains (here .Z).
|
||||||
func (s *state) evalField(data reflect.Value, fieldName string, args []node, final reflect.Value,
|
func (s *state) evalField(dot reflect.Value, fieldName string, args []node, final reflect.Value,
|
||||||
canBeMethod bool) reflect.Value {
|
receiver reflect.Value) reflect.Value {
|
||||||
typ := data.Type()
|
typ := dot.Type()
|
||||||
var isNil bool
|
if receiver.IsValid() {
|
||||||
data, isNil = indirect(data)
|
receiver, _ = indirect(receiver)
|
||||||
if canBeMethod {
|
|
||||||
// Need to get to a value of type *T to guarantee we see all
|
// Need to get to a value of type *T to guarantee we see all
|
||||||
// methods of T and *T.
|
// methods of T and *T.
|
||||||
ptr := data
|
ptr := receiver
|
||||||
if ptr.CanAddr() {
|
if ptr.CanAddr() {
|
||||||
ptr = ptr.Addr()
|
ptr = ptr.Addr()
|
||||||
}
|
}
|
||||||
if method, ok := methodByName(ptr.Type(), fieldName); ok {
|
if method, ok := methodByName(ptr.Type(), fieldName); ok {
|
||||||
return s.evalCall(ptr, method.Func, fieldName, true, args, final)
|
return s.evalCall(dot, ptr, method.Func, fieldName, args, final)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// It's not a method; is it a field of a struct?
|
// It's not a method; is it a field of a struct?
|
||||||
if data.Kind() == reflect.Struct {
|
dot, isNil := indirect(dot)
|
||||||
field := data.FieldByName(fieldName)
|
if dot.Kind() == reflect.Struct {
|
||||||
|
field := dot.FieldByName(fieldName)
|
||||||
if field.IsValid() {
|
if field.IsValid() {
|
||||||
if len(args) > 1 || final.IsValid() {
|
if len(args) > 1 || final.IsValid() {
|
||||||
s.errorf("%s is not a method but has arguments", fieldName)
|
s.errorf("%s is not a method but has arguments", fieldName)
|
||||||
@ -411,8 +411,9 @@ var (
|
|||||||
osErrorType = reflect.TypeOf(new(os.Error)).Elem()
|
osErrorType = reflect.TypeOf(new(os.Error)).Elem()
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *state) evalCall(v, fun reflect.Value, name string, isMethod bool, args []node, final reflect.Value) reflect.Value {
|
func (s *state) evalCall(dot, receiver, fun reflect.Value, name string, args []node, final reflect.Value) reflect.Value {
|
||||||
typ := fun.Type()
|
typ := fun.Type()
|
||||||
|
isMethod := receiver.IsValid()
|
||||||
if !isMethod && len(args) > 0 { // Args will be nil if it's a niladic call in an argument list
|
if !isMethod && len(args) > 0 { // Args will be nil if it's a niladic call in an argument list
|
||||||
args = args[1:] // first arg is name of function; not used in call.
|
args = args[1:] // first arg is name of function; not used in call.
|
||||||
}
|
}
|
||||||
@ -437,18 +438,18 @@ func (s *state) evalCall(v, fun reflect.Value, name string, isMethod bool, args
|
|||||||
// First arg is the receiver.
|
// First arg is the receiver.
|
||||||
i := 0
|
i := 0
|
||||||
if isMethod {
|
if isMethod {
|
||||||
argv[0] = v
|
argv[0] = receiver
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
// Others must be evaluated. Fixed args first.
|
// Others must be evaluated. Fixed args first.
|
||||||
for ; i < numFixed; i++ {
|
for ; i < numFixed; i++ {
|
||||||
argv[i] = s.evalArg(v, typ.In(i), args[i])
|
argv[i] = s.evalArg(dot, typ.In(i), args[i])
|
||||||
}
|
}
|
||||||
// And now the ... args.
|
// And now the ... args.
|
||||||
if typ.IsVariadic() {
|
if typ.IsVariadic() {
|
||||||
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
|
argType := typ.In(typ.NumIn() - 1).Elem() // Argument is a slice.
|
||||||
for ; i < len(args); i++ {
|
for ; i < len(args); i++ {
|
||||||
argv[i] = s.evalArg(v, argType, args[i])
|
argv[i] = s.evalArg(dot, argType, args[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Add final value if necessary.
|
// Add final value if necessary.
|
||||||
@ -474,14 +475,14 @@ func (s *state) validateType(value reflect.Value, typ reflect.Type) reflect.Valu
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Value {
|
func (s *state) evalArg(dot reflect.Value, typ reflect.Type, n node) reflect.Value {
|
||||||
switch arg := n.(type) {
|
switch arg := n.(type) {
|
||||||
case *dotNode:
|
case *dotNode:
|
||||||
return s.validateType(data, typ)
|
return s.validateType(dot, typ)
|
||||||
case *fieldNode:
|
case *fieldNode:
|
||||||
return s.validateType(s.evalFieldNode(data, arg, []node{n}, zero), typ)
|
return s.validateType(s.evalFieldNode(dot, arg, []node{n}, zero), typ)
|
||||||
case *variableNode:
|
case *variableNode:
|
||||||
return s.validateType(s.evalVariableNode(arg, nil, zero), typ)
|
return s.validateType(s.evalVariableNode(dot, arg, nil, zero), typ)
|
||||||
}
|
}
|
||||||
switch typ.Kind() {
|
switch typ.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
@ -494,7 +495,7 @@ func (s *state) evalArg(data reflect.Value, typ reflect.Type, n node) reflect.Va
|
|||||||
return s.evalInteger(typ, n)
|
return s.evalInteger(typ, n)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if typ.NumMethod() == 0 {
|
if typ.NumMethod() == 0 {
|
||||||
return s.evalEmptyInterface(data, n)
|
return s.evalEmptyInterface(dot, n)
|
||||||
}
|
}
|
||||||
case reflect.String:
|
case reflect.String:
|
||||||
return s.evalString(typ, n)
|
return s.evalString(typ, n)
|
||||||
@ -565,16 +566,16 @@ func (s *state) evalComplex(typ reflect.Type, n node) reflect.Value {
|
|||||||
panic("not reached")
|
panic("not reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) evalEmptyInterface(data reflect.Value, n node) reflect.Value {
|
func (s *state) evalEmptyInterface(dot reflect.Value, n node) reflect.Value {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *boolNode:
|
case *boolNode:
|
||||||
return reflect.ValueOf(n.true)
|
return reflect.ValueOf(n.true)
|
||||||
case *dotNode:
|
case *dotNode:
|
||||||
return data
|
return dot
|
||||||
case *fieldNode:
|
case *fieldNode:
|
||||||
return s.evalFieldNode(data, n, nil, zero)
|
return s.evalFieldNode(dot, n, nil, zero)
|
||||||
case *identifierNode:
|
case *identifierNode:
|
||||||
return s.evalFunction(data, n.ident, nil, zero)
|
return s.evalFunction(dot, n.ident, nil, zero)
|
||||||
case *numberNode:
|
case *numberNode:
|
||||||
if n.isComplex {
|
if n.isComplex {
|
||||||
return reflect.ValueOf(n.complex128)
|
return reflect.ValueOf(n.complex128)
|
||||||
@ -591,7 +592,7 @@ func (s *state) evalEmptyInterface(data reflect.Value, n node) reflect.Value {
|
|||||||
case *stringNode:
|
case *stringNode:
|
||||||
return reflect.ValueOf(n.text)
|
return reflect.ValueOf(n.text)
|
||||||
case *variableNode:
|
case *variableNode:
|
||||||
return s.evalVariableNode(n, nil, zero)
|
return s.evalVariableNode(dot, n, nil, zero)
|
||||||
}
|
}
|
||||||
s.errorf("can't handle assignment of %s to empty interface argument", n)
|
s.errorf("can't handle assignment of %s to empty interface argument", n)
|
||||||
panic("not reached")
|
panic("not reached")
|
||||||
|
@ -285,6 +285,10 @@ var execTests = []execTest{
|
|||||||
// Error handling.
|
// Error handling.
|
||||||
{"error method, error", "{{.EPERM true}}", "", tVal, false},
|
{"error method, error", "{{.EPERM true}}", "", tVal, false},
|
||||||
{"error method, no error", "{{.EPERM false}}", "false", tVal, true},
|
{"error method, no error", "{{.EPERM false}}", "false", tVal, true},
|
||||||
|
|
||||||
|
// Fixed bugs.
|
||||||
|
// Must separate dot and receiver; otherwise args are evaluated with dot set to variable.
|
||||||
|
{"problem", "{{range .MSIone}}-{{if $.Method1 .}}X{{end}}{{end}}-", "-X-", tVal, true},
|
||||||
}
|
}
|
||||||
|
|
||||||
func zeroArgs() string {
|
func zeroArgs() string {
|
||||||
|
Loading…
Reference in New Issue
Block a user