Skip to content

Commit 220cf6c

Browse files
committed
feat(injector/join): Introduce finalResultImplements for final return value interface matching
This update adds the `finalResultImplements` FunctionOption, enabling the matching of functions where the final return value implements a specified interface. The implementation includes methods for determining package and file match types, as well as evaluating the final return value against the target interface. Additionally, the configuration YAML has been updated with test cases demonstrating the new functionality, including scenarios that illustrate the distinction between `result-implements` and `final-result-implements`. Signed-off-by: Kemal Akkoyun <kemal.akkoyun@datadoghq.com>
1 parent 8928a9f commit 220cf6c

3 files changed

Lines changed: 202 additions & 0 deletions

File tree

internal/injector/aspect/join/function.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,66 @@ func (fo *resultImplements) Hash(h *fingerprint.Hasher) error {
428428
return h.Named("result-implements", fingerprint.String(fo.InterfaceName))
429429
}
430430

431+
// finalResultImplements matches functions where specifically the final return value
432+
// implements the specified interface.
433+
type finalResultImplements struct {
434+
InterfaceName string
435+
}
436+
437+
// FinalResultImplements creates a FunctionOption that matches functions where the final
438+
// return value implements the named interface.
439+
func FinalResultImplements(interfaceName string) FunctionOption {
440+
return &finalResultImplements{InterfaceName: interfaceName}
441+
}
442+
443+
func (fo *finalResultImplements) impliesImported() []string {
444+
pkgPath, _ := typed.SplitPackageAndName(fo.InterfaceName)
445+
if pkgPath != "" {
446+
return []string{pkgPath}
447+
}
448+
return nil
449+
}
450+
451+
func (_ *finalResultImplements) packageMayMatch(_ *may.PackageContext) may.MatchType {
452+
// Cannot reliably determine possibility of match based on package imports
453+
// due to structural typing. A type can implement an interface without
454+
// importing the interface's package.
455+
return may.Unknown
456+
}
457+
458+
func (_ *finalResultImplements) fileMayMatch(_ *may.FileContext) may.MatchType {
459+
// Cannot reliably determine possibility of match based on file contents
460+
// due to structural typing and type aliases.
461+
return may.Unknown
462+
}
463+
464+
func (fo *finalResultImplements) evaluate(info functionInformation) bool {
465+
if info.Type.Results == nil || len(info.Type.Results.List) == 0 {
466+
// No return values, no match.
467+
return false
468+
}
469+
470+
// Ensure the type resolver is available.
471+
if info.typeResolver == nil {
472+
return false
473+
}
474+
475+
// Resolve the target interface name (e.g., "io.Reader", "error") to a types.Interface.
476+
targetInterface, err := typed.ResolveInterfaceTypeByName(fo.InterfaceName)
477+
if err != nil {
478+
// If the interface name is invalid or cannot be resolved, we cannot match.
479+
return false
480+
}
481+
482+
// Check if the last field implements the interface.
483+
lastField := info.Type.Results.List[len(info.Type.Results.List)-1]
484+
return typed.ExprImplements(info.typeResolver, lastField.Type, targetInterface)
485+
}
486+
487+
func (fo *finalResultImplements) Hash(h *fingerprint.Hasher) error {
488+
return h.Named("final-result-implements", fingerprint.String(fo.InterfaceName))
489+
}
490+
431491
func init() {
432492
unmarshalers["function-body"] = func(ctx gocontext.Context, node ast.Node) (Point, error) {
433493
up, err := FromYAML(ctx, node)
@@ -543,6 +603,16 @@ func (o *unmarshalFuncDeclOption) UnmarshalYAML(ctx gocontext.Context, node ast.
543603
}
544604
// NOTE: Validation happens later during type resolution.
545605
o.FunctionOption = ResultImplements(ifaceName)
606+
case "final-result-implements":
607+
var ifaceName string
608+
if err := yaml.NodeToValueContext(ctx, mapping.Values[0].Value, &ifaceName); err != nil {
609+
return err
610+
}
611+
if ifaceName == "" {
612+
return fmt.Errorf("line %d: 'final-result-implements' cannot be empty", node.GetToken().Position.Line)
613+
}
614+
// NOTE: Validation happens later during type resolution.
615+
o.FunctionOption = FinalResultImplements(ifaceName)
546616
default:
547617
return fmt.Errorf("unknown FuncDeclOption name: %q", key)
548618
}

internal/injector/testdata/injector/result-implements-join/config.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,67 @@ aspects:
3232
template: |
3333
fmt.Println("Success: instrumented returnsError implementer")
3434
35+
# Aspect 3: Target functions where the final return value implements io.Reader
36+
- id: "instrument-final-reader-returns"
37+
join-point:
38+
function-body:
39+
function:
40+
- final-result-implements: "io.Reader" # ...of functions where the final return value implements io.Reader
41+
advice:
42+
- prepend-statements:
43+
imports:
44+
fmt: fmt
45+
template: |
46+
fmt.Println("Success: instrumented final returnsReader implementer")
47+
48+
# Aspect 4: Target functions where the final return value implements error
49+
- id: "instrument-final-error-returns"
50+
join-point:
51+
function-body:
52+
function:
53+
- final-result-implements: "error" # ...of functions where the final return value implements error
54+
advice:
55+
- prepend-statements:
56+
imports:
57+
fmt: fmt
58+
template: |
59+
fmt.Println("Success: instrumented final returnsError implementer")
60+
61+
# Aspect 5: Demonstrate difference between result-implements and final-result-implements
62+
- id: "demonstrate-difference"
63+
join-point:
64+
function-body:
65+
function:
66+
- name: returnsErrorThenString # Target specific function for clarity
67+
advice:
68+
- prepend-statements:
69+
imports:
70+
fmt: fmt
71+
template: |
72+
fmt.Println("Demonstrating difference between result-implements and final-result-implements:")
73+
{{ $resultMatches := false }}
74+
{{ $finalMatches := false }}
75+
76+
{{ with .Function.ResultThatImplements "error" }}
77+
{{ $resultMatches = true }}
78+
{{ end }}
79+
80+
{{ if .Function.FinalResultImplements "error" }}
81+
{{ $finalMatches = true }}
82+
{{ end }}
83+
84+
{{ if $resultMatches }}
85+
fmt.Println("Success: result-implements finds error in non-final position")
86+
{{ else }}
87+
fmt.Println("Failure: result-implements didn't find error in function")
88+
{{ end }}
89+
90+
{{ if not $finalMatches }}
91+
fmt.Println("Success: final-result-implements correctly ignores non-final error")
92+
{{ else }}
93+
fmt.Println("Failure: final-result-implements incorrectly matched non-final error")
94+
{{ end }}
95+
3596
3697
code: |-
3798
package main
@@ -145,5 +206,12 @@ code: |-
145206
return "neither implements", 5
146207
}
147208
209+
// This function has an error but it's not the final return value
210+
// Should match ResultImplements("error") but not FinalResultImplements("error")
211+
func returnsErrorThenString() (error, string) {
212+
fmt.Println("Executing returnsErrorThenString")
213+
return errors.New("error in non-final position"), "final non-error value"
214+
}
215+
148216
// Main - needed for compilation by the test harness.
149217
func main() {}

internal/injector/testdata/injector/result-implements-join/modified.go.snap

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ type myCustomReaderImpl struct {
1818

1919
func (cr myCustomReaderImpl) Read(p []byte) (int, error) {
2020
//line <generated>:1
21+
{
22+
fmt.Println("Success: instrumented final returnsError implementer")
23+
24+
}
2125
{
2226
fmt.Println("Success: instrumented returnsError implementer")
2327

@@ -40,6 +44,10 @@ func (e myCustomError) Error() string {
4044

4145
func returnsReader() io.Reader {
4246
//line <generated>:1
47+
{
48+
fmt.Println("Success: instrumented final returnsReader implementer")
49+
50+
}
4351
{
4452
fmt.Println("Success: instrumented returnsReader implementer")
4553

@@ -51,6 +59,10 @@ func returnsReader() io.Reader {
5159

5260
func returnsBuffer() *bytes.Buffer {
5361
//line <generated>:1
62+
{
63+
fmt.Println("Success: instrumented final returnsReader implementer")
64+
65+
}
5466
{
5567
fmt.Println("Success: instrumented returnsReader implementer")
5668

@@ -62,6 +74,10 @@ func returnsBuffer() *bytes.Buffer {
6274

6375
func returnsError() error {
6476
//line <generated>:1
77+
{
78+
fmt.Println("Success: instrumented final returnsError implementer")
79+
80+
}
6581
{
6682
fmt.Println("Success: instrumented returnsError implementer")
6783

@@ -73,6 +89,10 @@ func returnsError() error {
7389

7490
func returnsHttpError() error {
7591
//line <generated>:1
92+
{
93+
fmt.Println("Success: instrumented final returnsError implementer")
94+
95+
}
7696
{
7797
fmt.Println("Success: instrumented returnsError implementer")
7898

@@ -85,6 +105,10 @@ func returnsHttpError() error {
85105

86106
func returnsCustomReaderImpl() myCustomReaderImpl {
87107
//line <generated>:1
108+
{
109+
fmt.Println("Success: instrumented final returnsReader implementer")
110+
111+
}
88112
{
89113
fmt.Println("Success: instrumented returnsReader implementer")
90114

@@ -96,6 +120,10 @@ func returnsCustomReaderImpl() myCustomReaderImpl {
96120

97121
func returnsCustomError() myCustomError {
98122
//line <generated>:1
123+
{
124+
fmt.Println("Success: instrumented final returnsError implementer")
125+
126+
}
99127
{
100128
fmt.Println("Success: instrumented returnsError implementer")
101129

@@ -120,6 +148,10 @@ func noReturn() {
120148

121149
func returnsIntAndReader() (int, io.Reader) {
122150
//line <generated>:1
151+
{
152+
fmt.Println("Success: instrumented final returnsReader implementer")
153+
154+
}
123155
{
124156
fmt.Println("Success: instrumented returnsReader implementer")
125157

@@ -142,6 +174,10 @@ func returnsReaderAndInt() (io.Reader, int) {
142174

143175
func returnsIntAndError() (int, error) {
144176
//line <generated>:1
177+
{
178+
fmt.Println("Success: instrumented final returnsError implementer")
179+
180+
}
145181
{
146182
fmt.Println("Success: instrumented returnsError implementer")
147183

@@ -164,6 +200,10 @@ func returnsErrorAndInt() (error, int) {
164200

165201
func returnsMultipleReaders() (*bytes.Buffer, io.Reader) {
166202
//line <generated>:1
203+
{
204+
fmt.Println("Success: instrumented final returnsReader implementer")
205+
206+
}
167207
{
168208
fmt.Println("Success: instrumented returnsReader implementer")
169209

@@ -175,6 +215,10 @@ func returnsMultipleReaders() (*bytes.Buffer, io.Reader) {
175215

176216
func returnsMultipleErrors() (error, error) {
177217
//line <generated>:1
218+
{
219+
fmt.Println("Success: instrumented final returnsError implementer")
220+
221+
}
178222
{
179223
fmt.Println("Success: instrumented returnsError implementer")
180224

@@ -189,5 +233,25 @@ func returnsStringAndInt() (string, int) {
189233
return "neither implements", 5
190234
}
191235

236+
// This function has an error but it's not the final return value
237+
// Should match ResultImplements("error") but not FinalResultImplements("error")
238+
func returnsErrorThenString() (__result__0 error, _ string) {
239+
//line <generated>:1
240+
{
241+
fmt.Println("Demonstrating difference between result-implements and final-result-implements:")
242+
243+
fmt.Println("Success: result-implements finds error in non-final position")
244+
245+
fmt.Println("Success: final-result-implements correctly ignores non-final error")
246+
}
247+
{
248+
fmt.Println("Success: instrumented returnsError implementer")
249+
250+
}
251+
//line input.go:115
252+
fmt.Println("Executing returnsErrorThenString")
253+
return errors.New("error in non-final position"), "final non-error value"
254+
}
255+
192256
// Main - needed for compilation by the test harness.
193257
func main() {}

0 commit comments

Comments
 (0)