-
Notifications
You must be signed in to change notification settings - Fork 295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[fx-annotate] Verify invalid tags passed to fx.Annotate #835 #1051
[fx-annotate] Verify invalid tags passed to fx.Annotate #835 #1051
Conversation
annotated.go
Outdated
for _, tag := range pt.tags { | ||
if strings.Contains(tag, "'") { | ||
correctedTag := strings.ReplaceAll(tag, "'", "\"") | ||
return errors.New("the ParamTags tag has invalid quotations, did you mean " + correctedTag + " ?") | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, just doing a quick drive-by.
This seems a bit too specific. What if my tag value has '
in it?
type foo struct {
Value string `name:"a'bc"`
Fx does not have any rules rejecting that, AFAIK.
Y'all might be able to borrow struct tag validation from the structtag lint check in go vet
.
https://cs.opensource.google/go/go/+/master:src/cmd/vendor/golang.org/x/tools/go/analysis/passes/structtag/structtag.go;drc=31881f87873b84709a49ca17195bbe5b3f683acf;l=212
(We don't need the xml/json specialization there.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @abhinav, that's really helpful! I'll look into that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw this is still a draft but some initial thoughts :)
annotated.go
Outdated
for _, tag := range rt.tags { | ||
if strings.Contains(tag, "'") { | ||
correctedTag := strings.ReplaceAll(tag, "'", "\"") | ||
return errors.New("the ResultTags tag has invalid quotations, did you mean " + correctedTag + " ?") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to surround correctedTag
with quotes or backticks to help demarcate it in the error message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, good catch thanks!
annotated_test.go
Outdated
require.Error(t, err) | ||
assert.Contains(t, err.Error(), `the ParamTags tag has invalid quotations, did you mean name:"something" ?`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thoughts on using ErrorContains instead? It looks like it can combine all 3 of these lines in one.
annotated_test.go
Outdated
@@ -1347,6 +1347,78 @@ func TestAnnotate(t *testing.T) { | |||
assert.Contains(t, err.Error(), "encountered error while applying annotation using fx.Annotate to go.uber.org/fx_test.TestAnnotate.func1(): cannot apply more than one line of ResultTags") | |||
}) | |||
|
|||
t.Run("invalid ParamTag quotations", func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to @abhinav 's comment about additional error cases that these tests don't cover.
I also think we can convert these tests to table tests since much of the logic is the same between tests (https://github.com/uber-go/guide/blob/master/style.md#test-tables)
i.e. the test case struct could look something like
type test struct {
name string
giveAnnotation fx.Annotation // set this to result of either fx.ParamTags or fx.ResultTags
wantErr string
}
Codecov Report
@@ Coverage Diff @@
## master #1051 +/- ##
==========================================
+ Coverage 98.50% 98.52% +0.02%
==========================================
Files 39 39
Lines 2937 2989 +52
==========================================
+ Hits 2893 2945 +52
Misses 37 37
Partials 7 7
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
9dea8c4
to
14a3cc6
Compare
c647465
to
1c3d56b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Submitting on behalf of @r-hang for group code review.
Let's try to refactor this:
- Let's try to separate separate failure cases into different functions to make the code easier to understand
- Let's try without the "did you mean?" suggestions for now, for this case it's probably not worth the added complexity
annotated_test.go
Outdated
}{ | ||
{ | ||
give: "ParamTags wrong ending quote", | ||
wantErr: errTagValueSyntaxEndingQuote + "`name:\"something\"` ?", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Considering specifying in the test message whether it is the key or the value that has the incorrect quote
wantErr: errTagValueSpace, | ||
giveAnnotation: fx.ParamTags(`name: "something"`), | ||
}, | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The set of tests for ResultTags looks similar to for ParamTags, can we de-duplicate these tests?
annotated_test.go
Outdated
err := app.Err() | ||
assert.ErrorContains(t, err, tt.wantErr) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can combine these
assert.ErrorContains(t, app.Err(), tt.wantErr)
annotated_test.go
Outdated
}) | ||
} | ||
|
||
tests1 := []struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's put two separate sub-tests, one for your valid cases and one for your invalid cases
annotated_test.go
Outdated
{ | ||
give: "ParamTags Tag Empty with extra spaces", | ||
giveAnnotation: fx.ParamTags(`name:"versionNum"`, ` `), | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
repeated test cases
annotated_test.go
Outdated
err := app.Err() | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as above, can combine these
7bc8882
to
0ad3a6c
Compare
annotated.go
Outdated
// Given a tag with key, value pair, they must be separaed by a space. | ||
// Eg: `group:"some" optional:"true"` | ||
// return an error if in the case of multiple tags, they aren't separated | ||
// space. Return nil on success. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think about this shortened form?
// Given a tag with key, value pair, they must be separaed by a space. | |
// Eg: `group:"some" optional:"true"` | |
// return an error if in the case of multiple tags, they aren't separated | |
// space. Return nil on success. | |
// Collections of key value pairs within a tag should be separated by a space. | |
// Eg: `group:"some" optional:"true"` |
I think we can drop the comments on the function returns since they can be picked up quickly from the code.
annotated.go
Outdated
// return an error if in the case of multiple tags, they aren't separated | ||
// space. Return nil on success. | ||
func verifyTagsSpaceSeparated(numTags int, tag string) error { | ||
if numTags > 0 && tag != "" && tag[0] != ' ' { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
numTags makes me think it's the number of tags in the annotation and that check should be > 1. I think something like tagIdx might make the purposes clearer?
annotated.go
Outdated
// Given a value from a tag verify that it is contained between double quotes | ||
// Return an error if the invalid quote format is used for tag value. | ||
// On sucess return the remaining tag with nil error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are your thoughts on a shorter comment like "verify tag values are delimited with double quotes"?
I think we can drop documentation on the error return value since it's clear that should happen if the validation fails.
annotated.go
Outdated
func verifyAnnotateTag(tag string) error { | ||
numTags := 0 | ||
for ; tag != ""; numTags++ { | ||
validKeys := map[string]bool{"group": true, "optional": true, "name": true} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: we commonly use map[string]struct{} to map[string]bool as an empty struct consumes zero bytes of storage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! That'd be really handy
annotated.go
Outdated
return err | ||
} | ||
i := 0 | ||
// return if tag is empty |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this comment can be removed.
annotated.go
Outdated
i++ | ||
} | ||
key := strings.TrimSpace(tag[:i]) | ||
// if key is not equal to group name or optional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the comment can be removed given the good naming of the map lookup
// Return an error if the invalid quote format is used for tag value. | ||
// On sucess return the remaining tag with nil error | ||
func verifyValueQuote(value string) (string, error) { | ||
// starting quote should be a double quote |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we just check if the value string is prefixed and suffixed with double quotes. Could the step to validate tag values is within quotes be replaced by counting the total number of double quotes and ensuring that is exactly 2?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The total number of double quotes can be more than 2, as it's valid for a struct string to have quotes in it.
key:"val/"ue"
would be a valid tag with more than 2 double quotes. Hence, I don't think it'd be possible unless I misunderstood?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah understood that this approach does not work because value actually contains the rest of the tag strings.
f541237
to
49cd76d
Compare
annotated.go
Outdated
func verifyAnnotateTag(tag string) error { | ||
tagIdx := 0 | ||
for ; tag != ""; tagIdx++ { | ||
validKeys := map[string]struct{}{"group": {}, "optional": {}, "name": {}} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we move this map to a top-level variable since those values seems pretty static and we can prevent re-initializing on every loop?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Great catch!
annotated.go
Outdated
@@ -144,6 +144,75 @@ type paramTagsAnnotation struct { | |||
} | |||
|
|||
var _ Annotation = paramTagsAnnotation{} | |||
var ( | |||
errTagSyntaxSpace = errors.New(` multiple tags are not separated by space`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are the spaces needed here and in the next line?
annotated_test.go
Outdated
}, | ||
} | ||
for _, tt := range tests { | ||
t.Run(tt.give, func(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we're already running additional subtests what are your thoughts on prefixing the description string with the param or result case.
49cd76d
to
592d0b1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm modulo some minor fixups!
} | ||
for _, tt := range tests { | ||
t.Run(tt.give, func(t *testing.T) { | ||
app := NewForTest(t, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you're also using t.Parallel() at the top-level. You'll also need to call t.Parallel() within these subtests if you want them to run in parallel.
annotated.go
Outdated
// Given a string tag the function checks if the tag follows valid struct | ||
// format and returns an error if it's invalid. (i.e. not following | ||
// tag:"value" space-separated list ) | ||
// Currently dig accepts only 'name', 'group', 'optional' as valid tag keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Currently dig accepts only 'name', 'group', 'optional' as valid tag keys | |
// Currently dig accepts only 'name', 'group', 'optional' as valid tag keys. |
annotated.go
Outdated
@@ -285,10 +362,18 @@ var _ Annotation = resultTagsAnnotation{} | |||
// | |||
// If there has already been a ResultTag that was applied, this | |||
// will return an error. | |||
// | |||
// If the tag is invalid and has mismatched quotation for example, | |||
// (`tag_name:"tag_value') , this will return an error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// (`tag_name:"tag_value') , this will return an error | |
// (`tag_name:"tag_value') , this will return an error. |
annotated.go
Outdated
// If the tag is invalid and has mismatched quotation for example, | ||
// (`tag_name:"tag_value') , this will return an error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// If the tag is invalid and has mismatched quotation for example, | |
// (`tag_name:"tag_value') , this will return an error | |
// If the tag is invalid and has mismatched quotation for example, | |
// (`tag_name:"tag_value') , this will return an error. |
annotated.go
Outdated
return nil | ||
} | ||
|
||
// verify tag values are delimited with double quotes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// verify tag values are delimited with double quotes | |
// verify tag values are delimited with double quotes. |
b248a81
to
172f322
Compare
Some tags passed with fx.Annotate follow the struct format ``` fx.Annotate(f, fx.ParamTags(`group:”myGroup”)) ``` Where the tag is of the format `<tag_name>:”tag_value”` and the tag values must be contained within double quotes. If a user accidentally passes an invalid tag with mismatched quotes, the issue is not identified and no error is thrown. This is not desired as the tag gets passed to dig, where it’s ignored as it’s not a valid group tag. This commit adds checks to identify if invalid tags are passed to fx.Annotate. If identified then it throws an error with the right tag format specified. For e.g. - ``` fx.Annotate(f, fx.ParamTags(`group:'myGroup"`)) ``` Should be an invalid tag as it starts from a single quote and ends with a double quote (mismatched quotes instead of “..”) We added checks for ParamTag and ResultTag to verify and capture this invalid tag. Now, when we pass this invalid tag we get the following error : ``` Encountered error while applying annotation using fx.Annotate to function, tag value should start with double quote ``` Refs : uber-go#835
172f322
to
9e136fd
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Tags passed with fx.Annotate follow the struct format
Where the tag is of the format
<tag_name>:”tag_value”
and tags are separated by space.If a user accidentally passes an invalid tag such as mismatched quotes(#835), the issue is not identified and no error is thrown. This is not desired as the tag gets passed to dig, where it’s ignored as it’s not a valid group tag.
This commit adds checks to identify if invalid tags are passed to fx.Annotate. If identified then it throws an error with the right tag format specified.
The following checks are now enforced -
group:"some" optional:"true"
vimFor e.g. -
Should be an invalid tag as it starts from a single quote and ends with a double quote (mismatched quotes instead of “...”) We added checks for ParamTag and ResultTag to verify and capture this invalid tag.
Now, when we pass this invalid tag we get the following error :
Components Modified
annotated.go - add checks to verify invalid tag usage
annotated_test.go - Add test cases to verify functionality of changes
module_test.go - FIx a typo in testcase
Refs : #835