Repository: ysugimoto/falco
Pull request #5 (2021-07-06 14:55:01)

Fuzzy time type
This PR implements fuzzy type checks for built-in functions.

in VCL, `IP`, `RTIME` and`TIME`types have compatibility to `STRING` type:

```
std.time("Mon, 02 Jan 2006 22:04:05 GMT", "Mon, 02 Jan 2006 22:04:05 GMT");
```

`std.time` wants `TIME` type on 2nd argument but actually, STRING type could be passed.

Additionally, build identifilers specify in `context` package and lookup on identifier linting.


Patch:
diff --git a/context/context.go b/context/context.go
index 442aa98..ac327f8 100644
--- a/context/context.go
+++ b/context/context.go
@@ -53,7 +53,7 @@ type Context struct {
 	Tables         map[string]*types.Table
 	Directors      map[string]*types.Director
 	Subroutines    map[string]*types.Subroutine
-	Identifiers    map[string]types.VCLType
+	Identifiers    map[string]struct{}
 	functions      Functions
 	Variables      Variables
 	RegexVariables map[string]int
@@ -67,7 +67,7 @@ func New() *Context {
 		Tables:         make(map[string]*types.Table),
 		Directors:      make(map[string]*types.Director),
 		Subroutines:    make(map[string]*types.Subroutine),
-		Identifiers:    make(map[string]types.VCLType),
+		Identifiers:    builtinIdentifiers(),
 		functions:      builtinFunctions(),
 		Variables:      predefinedVariables(),
 		RegexVariables: newRegexMatchedValues(),
diff --git a/context/identifilers.go b/context/identifilers.go
new file mode 100644
index 0000000..2af727a
--- /dev/null
+++ b/context/identifilers.go
@@ -0,0 +1,41 @@
+package context
+
+// Fastly predifned identifier list.
+// we listed as possible as found in Fastly document site,
+// but perhapse there are more builtin identifiers.
+func builtinIdentifiers() map[string]struct{} {
+	return map[string]struct{}{
+		// use for backend.ssl_check_cert
+		// https://developer.fastly.com/reference/vcl/declarations/backend/
+		"always": {},
+
+		// use for crypto.encrypt_xxx function cipher argument
+		// https://developer.fastly.com/reference/vcl/functions/cryptographic/crypto-encrypt-hex/
+		"aes128": {},
+		"aes192": {},
+		"aes256": {},
+
+		// use for crypto.encrypt_xxx function mode argument
+		// https://developer.fastly.com/reference/vcl/functions/cryptographic/crypto-encrypt-hex/
+		"cbc": {},
+		"ctr": {},
+
+		// use for crypto.encrypt_xxx function padding argument
+		// https://developer.fastly.com/reference/vcl/functions/cryptographic/crypto-encrypt-hex/
+		"pkcs7": {},
+		"nopad": {},
+
+		// use for digest.rsa_verify function hash_method argument
+		// https://developer.fastly.com/reference/vcl/functions/cryptographic/digest-rsa-verify/
+		"default": {},
+		"sha256":  {},
+		"sha384":  {},
+		"sha512":  {},
+
+		// use for digest.rsa_verify function base64_method argument
+		// https://developer.fastly.com/reference/vcl/functions/cryptographic/digest-rsa-verify/
+		"standard":  {},
+		"url":       {},
+		"url_nopad": {},
+	}
+}
diff --git a/docs/rules.md b/docs/rules.md
index e55c988..610981a 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -10,7 +10,7 @@ Acl syntax is:
 
 ```vcl
 acl (?<acl_name>[a-zA-Z0-9_]+) {
-  (?<inverse>!?)"(?<ip_address>[0-9\.]+)"(/?)(?<cidr_mask>[0-9]+);
+  (?<inverse>!?)"(?<ip_address>[0-9a-z\.:]+)"(/?)(?<cidr_mask>[0-9]+);
   ...
 }
 ```
@@ -27,6 +27,8 @@ acl internal {
 }
 ```
 
+Note: `ip_address` variable could specify ipv6 format like "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff".
+
 Fastly Document : https://developer.fastly.com/reference/vcl/declarations/acl/
 
 
diff --git a/linter/errors.go b/linter/errors.go
index d307642..6882253 100644
--- a/linter/errors.go
+++ b/linter/errors.go
@@ -46,11 +46,11 @@ func (e *LintError) Error() string {
 		ref = "\ndocument: " + e.Reference
 	}
 	if e.Token.File != "" {
-		file = " in " + e.Token.File
+		file = " in" + e.Token.File
 	}
 
 	msg := fmt.Sprintf(
-		"[%s] %s%s%sat line: %d, position: %d%s",
+		"[%s] %s%s%s at line: %d, position: %d%s",
 		e.Severity, e.Message, rule, file, e.Token.Line, e.Token.Position, ref,
 	)
 	return msg
diff --git a/linter/linter.go b/linter/linter.go
index 7188532..a7d9a44 100644
--- a/linter/linter.go
+++ b/linter/linter.go
@@ -1036,14 +1036,16 @@ func (l *Linter) lintSyntheticStatement(stmt *ast.SyntheticStatement, ctx *conte
 func (l *Linter) lintIdent(exp *ast.Ident, ctx *context.Context) types.Type {
 	v, err := ctx.Get(exp.Value)
 	if err != nil {
-		if _, ok := ctx.Acls[exp.Value]; ok {
+		if _, ok := ctx.Identifiers[exp.Value]; ok {
+			return types.IDType
+		} else if _, ok := ctx.Acls[exp.Value]; ok {
 			return types.AclType
 		} else if _, ok := ctx.Backends[exp.Value]; ok {
 			return types.BackendType
 		} else if _, ok := ctx.Tables[exp.Value]; ok {
 			return types.TableType
 		}
-		return types.IDType
+		l.Error(UndefinedVariable(exp.GetMeta(), exp.Value))
 	}
 	return v
 }
@@ -1177,6 +1179,7 @@ func (l *Linter) lintInfixExpression(exp *ast.InfixExpression, ctx *context.Cont
 	case "+":
 		// Plus operator behaves string concatenation.
 		// VCL accepts other types with implicit type conversion as following:
+		// IDENT   -> point value
 		// STRING  -> raw string
 		// INTEGER -> stringify
 		// FLOAT   -> stringify
@@ -1223,7 +1226,7 @@ func (l *Linter) lintIfExpression(exp *ast.IfExpression, ctx *context.Context) t
 	l.lintIfCondition(exp.Condition, ctx)
 	if err := pushRegexGroupVars(exp.Condition, ctx); err != nil {
 		err := &LintError{
-			Severity: WARNING,
+			Severity: INFO,
 			Token:    exp.Condition.GetMeta().Token,
 			Message:  err.Error(),
 		}
@@ -1269,6 +1272,7 @@ func (l *Linter) lintFunctionCallExpression(exp *ast.FunctionCallExpression, ctx
 		return types.NeverType
 	}
 
+	// lint empty arguments
 	if len(fn.Arguments) == 0 {
 		if len(exp.Arguments) > 0 {
 			err := &LintError{
@@ -1281,23 +1285,55 @@ func (l *Linter) lintFunctionCallExpression(exp *ast.FunctionCallExpression, ctx
 			}
 			l.Error(err.Match(FUNCTION_ARGUMENS).Ref(fn.Reference))
 		}
-	} else {
-		var argTypes []types.Type
-		for _, a := range fn.Arguments {
-			if len(a) == len(exp.Arguments) {
-				argTypes = a
-				break
-			}
-		}
-		if len(argTypes) == 0 {
-			l.Error(FunctionArgumentMismatch(
-				exp.Function.GetMeta(), exp.Function.String(),
-				len(fn.Arguments), len(exp.Arguments),
-			).Match(FUNCTION_ARGUMENS).Ref(fn.Reference))
+		return fn.Return
+	}
+
+	var argTypes []types.Type
+	for _, a := range fn.Arguments {
+		if len(a) == len(exp.Arguments) {
+			argTypes = a
+			break
 		}
+	}
+	if len(argTypes) == 0 {
+		l.Error(FunctionArgumentMismatch(
+			exp.Function.GetMeta(), exp.Function.String(),
+			len(fn.Arguments), len(exp.Arguments),
+		).Match(FUNCTION_ARGUMENS).Ref(fn.Reference))
+	}
+
+	for i, v := range argTypes {
+		arg := l.Lint(exp.Arguments[i], ctx)
 
-		for i, v := range argTypes {
-			arg := l.Lint(exp.Arguments[i], ctx)
+		switch v {
+		case types.TimeType:
+			// fuzzy type check: some builtin function expects TIME type,
+			// then actual argument type could be STRING because VCL TIME type could be parsed from STRING.
+			if !expectType(arg, types.TimeType, types.StringType) {
+				l.Error(FunctionArgumentTypeMismatch(
+					exp.Function.GetMeta(), exp.Function.String(), i+1, v, arg,
+				).Match(FUNCTION_ARGUMENT_TYPE).Ref(fn.Reference))
+			}
+			continue
+		case types.RTimeType:
+			// fuzzy type check: some builtin function expects RTIME type,
+			// then actual argument type could be STRING because VCL TIME type could be parsed from STRING.
+			if !expectType(arg, types.TimeType, types.StringType) {
+				l.Error(FunctionArgumentTypeMismatch(
+					exp.Function.GetMeta(), exp.Function.String(), i+1, v, arg,
+				).Match(FUNCTION_ARGUMENT_TYPE).Ref(fn.Reference))
+			}
+			continue
+		case types.IPType:
+			// fuzzy type check: some builtin function expects IP type,
+			// then actual argument type could be STRING because VCL TIME type could be parsed from STRING.
+			if !expectType(arg, types.IPType, types.StringType) {
+				l.Error(FunctionArgumentTypeMismatch(
+					exp.Function.GetMeta(), exp.Function.String(), i+1, v, arg,
+				).Match(FUNCTION_ARGUMENT_TYPE).Ref(fn.Reference))
+			}
+		default:
+			// Otherwise, strict type check
 			if v != arg {
 				l.Error(FunctionArgumentTypeMismatch(
 					exp.Function.GetMeta(), exp.Function.String(), i+1, v, arg,


Test patch:
diff --git a/linter/linter_test.go b/linter/linter_test.go
index 5dbbe9b..d13e158 100644
--- a/linter/linter_test.go
+++ b/linter/linter_test.go
@@ -1167,4 +1167,16 @@ sub foo {
 }`
 		assertError(t, input)
 	})
+
+	t.Run("fuzzy type check for TIME type argument", func(t *testing.T) {
+		input := `
+sub foo {
+	declare local var.S STRING;
+	declare local var.T TIME;
+	set var.S = "Mon, 02 Jan 2006 22:04:05 GMT";
+
+	set var.T = std.time(var.S, "Mon Jan 2 22:04:05 2006");
+}`
+		assertNoError(t, input)
+	})
 }