nebius/SWE-rebench-V2-PRs huggingface dataset:
Repository: VKCOM/noverify
Pull request #1097 (2021-08-24 09:38:26)

types: added support for union typehint


Patch:
diff --git a/src/types/typehint.go b/src/types/typehint.go
index ccbc515..48dec61 100644
--- a/src/types/typehint.go
+++ b/src/types/typehint.go
@@ -14,17 +14,30 @@ func NormalizedTypeHintTypes(normalizer Normalizer, typeNode ir.Node) Map {
 //
 // No normalization is performed.
 func TypeHintTypes(typeNode ir.Node) []Type {
-	n := typeNode
-
 	var results []Type
-	if nullable, ok := typeNode.(*ir.Nullable); ok {
-		n = nullable.Expr
-		results = make([]Type, 0, 2)
-		results = append(results, Type{Elem: "null"})
-	} else {
-		results = make([]Type, 0, 1)
+
+	switch n := typeNode.(type) {
+	case *ir.Union:
+		results = make([]Type, 0, len(n.Types))
+
+		for _, unionTyp := range n.Types {
+			results = append(results, handleSingleType(unionTyp))
+		}
+
+		return results
+
+	case *ir.Nullable:
+		return []Type{
+			{Elem: "null"},
+			handleSingleType(n.Expr),
+		}
+
+	default:
+		return []Type{handleSingleType(n)}
 	}
+}
 
+func handleSingleType(n ir.Node) Type {
 	// There is a trick here.
 	// Unlike with phpdoc types, having `integer` here
 	// means that we need to force it to be interpreted as
@@ -33,8 +46,5 @@ func TypeHintTypes(typeNode ir.Node) []Type {
 	if IsAlias(typ.Elem) {
 		typ.Elem = `\` + typ.Elem
 	}
-
-	results = append(results, typ)
-
-	return results
+	return typ
 }


Test patch:
diff --git a/src/tests/exprtype/php8_test.go b/src/tests/exprtype/php8_test.go
new file mode 100644
index 0000000..edc71a5
--- /dev/null
+++ b/src/tests/exprtype/php8_test.go
@@ -0,0 +1,53 @@
+package exprtype_test
+
+import (
+	"testing"
+)
+
+func TestExprTypeUnionParam(t *testing.T) {
+	code := `<?php
+class Foo {}
+class Boo {}
+
+function f(int|string $a) {
+  exprtype($a, 'int|string');
+}
+
+function f(int|string|float $a) {
+  exprtype($a, 'float|int|string');
+}
+
+function f(int|Foo|float $a) {
+  exprtype($a, '\Foo|float|int');
+}
+
+function f(Boo|Foo $a) {
+  exprtype($a, '\Boo|\Foo');
+}
+`
+	runPHP8ExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
+func TestExprTypeUnionReturn(t *testing.T) {
+	code := `<?php
+class Foo {}
+class Boo {}
+
+function f1(): int|string {}
+exprtype(f1(), 'int|string');
+
+function f2(): int|string|float {}
+exprtype(f2(), 'float|int|string');
+
+function f3(): int|Foo|float {}
+exprtype(f3(), '\Foo|float|int');
+
+function f4(): Boo|Foo {}
+exprtype(f4(), '\Boo|\Foo');
+`
+	runPHP8ExprTypeTest(t, &exprTypeTestParams{code: code})
+}
+
+func runPHP8ExprTypeTest(t *testing.T, params *exprTypeTestParams) {
+	exprTypeTestImpl(t, params, false)
+}
Repository: VKCOM/noverify
Pull request #1150 (2021-09-08 14:06:40)

types: added `future_queue` to KPHP types


Patch:
diff --git a/src/phpdoctypes/converter.go b/src/phpdoctypes/converter.go
index d3f6f6d..5a1baab 100644
--- a/src/phpdoctypes/converter.go
+++ b/src/phpdoctypes/converter.go
@@ -247,7 +247,7 @@ func (conv *TypeConverter) mapShapeType(params []phpdoc.TypeExpr, allowedMixing
 				continue
 			}
 
-			if conv.kphp && (typ.Elem == "any" || typ.Elem == "kmixed" || typ.Elem == "future") {
+			if conv.kphp && (typ.Elem == "any" || typ.Elem == "kmixed" || typ.Elem == "future" || typ.Elem == "future_queue") {
 				continue
 			}
 
diff --git a/src/types/normalizer.go b/src/types/normalizer.go
index b21ae4e..977d9d1 100644
--- a/src/types/normalizer.go
+++ b/src/types/normalizer.go
@@ -37,7 +37,7 @@ func (n Normalizer) normalizeType(typ *Type) {
 		return
 	}
 
-	if n.kphp && (typ.Elem == "any" || typ.Elem == "kmixed" || typ.Elem == "future") {
+	if n.kphp && (typ.Elem == "any" || typ.Elem == "kmixed" || typ.Elem == "future" || typ.Elem == "future_queue") {
 		// `any` is a special KPHP type that is more-or-less
 		// identical to `mixed|object`. In PHP, `mixed` already covers
 		// objects, so there is no need to add `object`.


Test patch:
diff --git a/src/tests/checkers/kphp_test.go b/src/tests/checkers/kphp_test.go
index c1026da..7b0caa3 100644
--- a/src/tests/checkers/kphp_test.go
+++ b/src/tests/checkers/kphp_test.go
@@ -31,9 +31,30 @@ func TestDifferentTypesAsUndefinedClass(t *testing.T) {
  * @param future<int> $e
  * @param tuple(?any) $f
  * @param future<?any> $g
- * @param shape(foo: ?any, boo: future<int>) $h
+ * @param future_queue<?any> $h
+ * @param shape(foo: ?any, boo: future<int>) $i
  */
-function f($a, $b, $c, $d, $e, $f, $g, $h) {}
+function f($a, $b, $c, $d, $e, $f, $g, $h, $i) {}
+
+/**
+ * @return any
+ */
+function f1() {}
+
+/**
+ * @return ?any
+ */
+function f2() {}
+
+/**
+ * @return future<any>
+ */
+function f3() {}
+
+/**
+ * @return shape(key: any)
+ */
+function f4() {}
 `)
 	test.RunAndMatch()
 }
Repository: VKCOM/noverify
Pull request #1196 (2022-09-09 13:13:11)

linter: update ignore reports for auto-gen files


Patch:
diff --git a/src/workspace/file.go b/src/workspace/file.go
index 9b798c9..4fa15d1 100644
--- a/src/workspace/file.go
+++ b/src/workspace/file.go
@@ -110,7 +110,8 @@ func FileIsAutoGenerated(contents []byte) bool {
 
 		looksLikeComment := strings.HasPrefix(l, "//") ||
 			strings.HasPrefix(l, "/*") ||
-			strings.HasPrefix(l, " *")
+			strings.HasPrefix(l, " *") ||
+			strings.HasPrefix(l, "#")
 		if !looksLikeComment {
 			continue
 		}
@@ -120,6 +121,7 @@ func FileIsAutoGenerated(contents []byte) bool {
 			strings.Contains(l, "don't edit") ||
 			strings.Contains(l, "do not modify") ||
 			strings.Contains(l, "don't modify")
+
 		autoGenerated = autoGenerated ||
 			strings.Contains(l, "auto-generated") ||
 			strings.Contains(l, "autogenerated") ||


Test patch:
diff --git a/src/workspace/files_test.go b/src/workspace/files_test.go
index edeb8d4..5f91d23 100644
--- a/src/workspace/files_test.go
+++ b/src/workspace/files_test.go
@@ -13,6 +13,12 @@ func TestFileIsAutoGenerated(t *testing.T) {
 			autogen: true,
 			contents: `<?php
 /** file is auto-generated; do not edit! */
+`,
+		},
+		{
+			autogen: true,
+			contents: `<?php
+# THIS FILE IS GENERATED BY PHP_GENERATOR. DO NOT EDIT!
 `,
 		},
 		{
Repository: VKCOM/noverify
Pull request #1204 (2022-10-26 10:12:54)

phpgrep: add ArrowFunctionExpr matching support


Patch:
diff --git a/src/phpgrep/matcher.go b/src/phpgrep/matcher.go
index 1f3c32f..389f495 100644
--- a/src/phpgrep/matcher.go
+++ b/src/phpgrep/matcher.go
@@ -755,6 +755,8 @@ func (m *matcher) eqNode(state *matcherState, x, y ir.Node) bool {
 			m.eqNode(state, x.DefaultValue, y.DefaultValue)
 	case *ir.ClosureExpr:
 		return m.eqClosure(state, x, y)
+	case *ir.ArrowFunctionExpr:
+		return m.eqArrowFunction(state, x, y)
 
 	case *ir.TernaryExpr:
 		return m.eqTernary(state, x, y)
@@ -827,6 +829,18 @@ func (m *matcher) eqTernary(state *matcherState, x *ir.TernaryExpr, y ir.Node) b
 	return false
 }
 
+func (m *matcher) eqArrowFunction(state *matcherState, x *ir.ArrowFunctionExpr, y ir.Node) bool {
+	if y, ok := y.(*ir.ArrowFunctionExpr); ok {
+		return x.ReturnsRef == y.ReturnsRef &&
+			x.Static == y.Static &&
+			m.eqNodeSlice(state, x.Params, y.Params) &&
+			m.eqNode(state, x.ReturnType, y.ReturnType) &&
+			m.eqNode(state, x.Expr, y.Expr)
+	}
+
+	return false
+}
+
 func (m *matcher) eqClosure(state *matcherState, x *ir.ClosureExpr, y ir.Node) bool {
 	if y, ok := y.(*ir.ClosureExpr); ok {
 		var xUses, yUses []ir.Node
@@ -836,7 +850,7 @@ func (m *matcher) eqClosure(state *matcherState, x *ir.ClosureExpr, y ir.Node) b
 		if y.ClosureUse != nil {
 			yUses = y.ClosureUse.Uses
 		}
-		return ok && x.ReturnsRef == y.ReturnsRef &&
+		return x.ReturnsRef == y.ReturnsRef &&
 			x.Static == y.Static &&
 			m.eqNodeSlice(state, x.Params, y.Params) &&
 			m.eqNode(state, x.ReturnType, y.ReturnType) &&


Test patch:
diff --git a/src/phpgrep/matcher_test.go b/src/phpgrep/matcher_test.go
index 1283f22..07b3015 100644
--- a/src/phpgrep/matcher_test.go
+++ b/src/phpgrep/matcher_test.go
@@ -570,6 +570,10 @@ func TestMatch(t *testing.T) {
 		{`function() { ${"*"}; return 1; }`, `function() { f(); return 1; }`},
 		{`function() { ${"*"}; return 1; }`, `function() { f(); f(); return 1; }`},
 
+		{`fn($x) => $x`, `fn($x) => $x`},
+		{`fn($x): int => $x`, `fn($x): int => $x`},
+		{`fn(int $x) => $x + 1`, `fn(int $x) => $x + 1`},
+
 		{`${"func"}`, `function() {}`},
 		{`${"func"}`, `function($x) {}`},
 		{`${"func"}`, `function() { return 1; }`},
@@ -733,6 +737,13 @@ func TestMatchNegative(t *testing.T) {
 		{`function() { ${"*"}; return 1; }`, `function($x) { return 1; }`},
 		{`function() { ${"*"}; return 1; }`, `static function() { f(); f(); return 1; }`},
 
+		{`fn($x) => $x`, `fn($x) => $x + 1`},
+		{`fn($x) => $x`, `fn($x) => $y`},
+		{`fn($x): int => $x`, `fn($x): string => $x`},
+		{`fn(int $x): int => $x + 1`, `fn(int $x) => $x + 1`},
+		{`fn(int $x) => $x + 1`, `fn(int $x): int => $x + 1`},
+		{`fn(int $x): int => $x + 1`, `fn(int $x): int => $x`},
+
 		{`!($x instanceof $y)`, `1`},
 		{`$x instanceof T1`, `$v instanceof T2`},
 		{`$x instanceof T`, `$x instanceof $y`},
Repository: VKCOM/noverify
Pull request #1210 (2023-03-30 17:25:18)

rules: fixed checking `@filter` in dynamic rules


Patch:
diff --git a/src/linter/root.go b/src/linter/root.go
index feafef3..3c23699 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -1826,8 +1826,8 @@ func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter
 		}
 		if filter.Regexp != nil {
 			if vr, ok := nn.(*ir.SimpleVar); ok {
-				if filter.Regexp.MatchString(vr.Name) {
-					return true
+				if !filter.Regexp.MatchString(vr.Name) {
+					return false
 				}
 			}
 		}


Test patch:
diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go
index 1e32fcc..da0c77f 100644
--- a/src/tests/rules/rules_test.go
+++ b/src/tests/rules/rules_test.go
@@ -419,26 +419,72 @@ function bad(string $x) {
 
 func TestRulesFilter(t *testing.T) {
 	rfile := `<?php
+function basic_rules() {
+  /**
+   * @warning Don't use $var variable
+   * @filter $var ^book_id$
+   */
+  $var;
+}
 function id_check() {
   /**
-   * @warning Don't use $id variable
-   * @filter $id ^id$
+   * @warning Don't use the name $id for the variable
+   * @filter $id ^(user|owner)_id$
+   */
+  any: {
+    $id ==  0;
+    $id === 0;
+  }
+}
+function type_type_rules() {
+  /**
+   * @warning Don't use $animal variable
+   * @type int $animal
+   * @filter $animal ^animal_(name|id)$
    */
-  $id;
+  $animal == 0;
 }
 `
 	test := linttest.NewSuite(t)
 	test.RuleFile = rfile
 	test.AddFile(`<?php
-$id = 100;
-echo $id;
-echo $id == 0;
-if ($id == 0) {}
+function basic_test() {
+  $book_id = 100;
+  echo $book_id;
+  echo $book_id == 0;
+  if ($book_id == 0) {}
+}
+function test(int $user_id, int $id, int $owner_id, int $chat_id, int $id_owner) {
+  $_ = $user_id === 0;
+  $_ = $user_id == 0;
+
+  $_ = $id === 0;
+  $_ = $id == 0;
+
+  $_ = $owner_id === 0;
+  $_ = $owner_id == 0;
+
+  $_ = $chat_id === 0;
+  $_ = $chat_id == 0;
+
+  $_ = $id_owner === 0;
+  $_ = $id_owner == 0;
+}
+function type_type_check(string $animal_name, int $animal_id) {
+  $_ = $animal_name == 0;
+
+  $_ = $animal_id == 0;
+}
 `)
 	test.Expect = []string{
-		`Don't use $id variable`,
-		`Don't use $id variable`,
-		`Don't use $id variable`,
+		`Don't use $book_id variable`,
+		`Don't use $book_id variable`,
+		`Don't use $book_id variable`,
+		`Don't use the name $user_id for the variable`,
+		`Don't use the name $user_id for the variable`,
+		`Don't use the name $owner_id for the variable`,
+		`Don't use the name $owner_id for the variable`,
+		`Don't use $animal_id variable`,
 	}
 	test.RunRulesTest()
 }
Repository: VKCOM/noverify
Pull request #1225 (2024-07-22 10:09:24)

linter: suppress in dynamic rules


Patch:
diff --git a/src/linter/root.go b/src/linter/root.go
index 261c85f..5849f15 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -172,6 +172,7 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 		if !strings.HasSuffix(n.InterfaceName.Value, "able") {
 			d.checker.CheckIdentMisspellings(n.InterfaceName)
 		}
+
 	case *ir.ClassStmt:
 		d.currentClassNodeStack.Push(n)
 
@@ -225,6 +226,8 @@ func (d *rootWalker) EnterNode(n ir.Node) (res bool) {
 
 		d.scope().AddVar(v, solver.ExprTypeLocal(d.scope(), d.ctx.st, n.Expr), "global variable", meta.VarAlwaysDefined)
 	case *ir.FunctionStmt:
+		d.currentClassNodeStack.Push(n)
+
 		if d.metaInfo().IsIndexingComplete() {
 			res = d.checker.CheckFunction(n)
 		} else {
@@ -1746,12 +1749,32 @@ func (d *rootWalker) renderRuleMessage(msg string, n ir.Node, m phpgrep.MatchDat
 	return msg
 }
 
+func (d *rootWalker) isSuppressed(n ir.Node, checkName string) bool {
+	if containLinterSuppress(n, checkName) {
+		return true
+	}
+
+	// We go up the tree in search of a comment that disables this checker.
+	for i := 0; d.currentClassNodeStack.NthParent(i) != nil; i++ {
+		parent := d.currentClassNodeStack.NthParent(i)
+		if containLinterSuppress(parent, checkName) {
+			return true
+		}
+	}
+
+	return false
+}
+
 func (d *rootWalker) runRule(n ir.Node, sc *meta.Scope, rule *rules.Rule) bool {
 	m, ok := rule.Matcher.Match(n)
 	if !ok {
 		return false
 	}
 
+	if d.isSuppressed(n, rule.Name) {
+		return false
+	}
+
 	matched := false
 	if len(rule.Filters) == 0 {
 		matched = true


Test patch:
diff --git a/src/tests/checkers/linter_suppress_test.go b/src/tests/checkers/linter_suppress_test.go
index 9e1aa6b..3335670 100644
--- a/src/tests/checkers/linter_suppress_test.go
+++ b/src/tests/checkers/linter_suppress_test.go
@@ -146,3 +146,77 @@ function g($b) {
 `)
 	test.RunAndMatch()
 }
+
+func TestLinterSuppressDNRInsideFunction(t *testing.T) {
+	rfile := `<?php
+/**
+ * @name emptyIf
+ * @warning suspicious empty body of the if statement
+ * @scope local
+ */
+if ($_);
+`
+
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+if (123); // No warning
+
+function f() {
+/**
+ * @noverify-suppress emptyIf
+ */
+  if (123); // Warning
+}
+`)
+	test.RunRulesTest()
+}
+
+func TestLinterSuppressDNRBehindFunction(t *testing.T) {
+	rfile := `<?php
+/**
+ * @name emptyIf
+ * @warning suspicious empty body of the if statement
+ * @scope local
+ */
+if ($_);
+`
+
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+if (123); // No warning
+/**
+ * @noverify-suppress emptyIf
+ */
+function f() {
+  if (123); // Warning
+}
+`)
+	test.RunRulesTest()
+}
+
+func TestLinterSuppressDNRMethod(t *testing.T) {
+	rfile := `<?php
+/**
+ * @name emptyIf
+ * @warning suspicious empty body of the if statement
+ * @scope local
+ */
+if ($_);
+`
+
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+class Foo {
+/**
+ * @noverify-suppress emptyIf
+ */
+public function f() {
+  if (123); // Warning
+}
+}
+`)
+	test.RunAndMatch()
+}
Repository: danielgtaylor/huma
Pull request #26 (2022-02-23 18:50:56)

fix: copy router into sub-resources
Tiny bug fix to make sure sub-resources get all fields copied. This gets used later when declaring operations on the sub-resources.


Patch:
diff --git a/resource.go b/resource.go
index b949842..31778d7 100644
--- a/resource.go
+++ b/resource.go
@@ -95,6 +95,7 @@ func (r *Resource) SubResource(path string) *Resource {
 	sub := &Resource{
 		path:         r.path + path,
 		mux:          r.mux.Route(path, nil),
+		router:       r.router,
 		subResources: []*Resource{},
 		operations:   []*Operation{},
 		tags:         append([]string{}, r.tags...),


Test patch:
diff --git a/router_test.go b/router_test.go
index 4e8cc4b..06fa090 100644
--- a/router_test.go
+++ b/router_test.go
@@ -400,3 +400,12 @@ func TestGetOperationDoesNotCrash(t *testing.T) {
 		assert.NotNil(t, info)
 	})
 }
+
+func TestSubResource(t *testing.T) {
+	app := newTestRouter()
+
+	// This should not crash.
+	app.Resource("/").SubResource("/foo").SubResource("/bar").Get("get-bar", "docs", NewResponse(http.StatusOK, "ok")).Run(func(ctx Context) {
+		// Do nothing
+	})
+}
Repository: danielgtaylor/huma
Pull request #57 (2022-07-27 19:07:24)

feat(graphql): add ignore prefixes config option
This PR adds a feature to ignore certain URL path prefixes when generating the GraphQL interface. This allows you to opt-out for some paths or groups of paths in the HTTP API, for example if they make no sense in the GraphQL interface or if multiple URL paths (temporarily) resolve to the same Huma handlers in your API.


Patch:
diff --git a/graphql.go b/graphql.go
index 1c92323..0bd7763 100644
--- a/graphql.go
+++ b/graphql.go
@@ -40,6 +40,10 @@ type GraphQLConfig struct {
 	// `GraphQLDefaultPaginator` is used.
 	Paginator GraphQLPaginator
 
+	// IgnorePrefixes defines path prefixes which should be ignored by the
+	// GraphQL model generator.
+	IgnorePrefixes []string
+
 	// known keeps track of known structs since they can only be defined once
 	// per GraphQL endpoint. If used by multiple HTTP operations, they must
 	// reference the same struct converted to GraphQL schema.
@@ -374,7 +378,13 @@ func (r *Router) EnableGraphQL(config *GraphQLConfig) {
 	config.costMap = gqlcost.CostMap{}
 	config.paginatorType = reflect.TypeOf(config.Paginator).Elem()
 
+outer:
 	for _, resource := range resources {
+		for _, prefix := range config.IgnorePrefixes {
+			if strings.HasPrefix(resource.path, prefix) {
+				continue outer
+			}
+		}
 		r.handleResource(config, "Query", fields, resource, map[string]bool{})
 	}
 


Test patch:
diff --git a/graphql_test.go b/graphql_test.go
index 7ecd71a..fb5fc59 100644
--- a/graphql_test.go
+++ b/graphql_test.go
@@ -194,8 +194,15 @@ func TestGraphQL(t *testing.T) {
 		ctx.WriteModel(http.StatusOK, categories[input.CategoryID].products[input.ProductID].stores[input.StoreID])
 	})
 
+	app.Resource("/ignored").Get("get-ignored", "doc",
+		NewResponse(http.StatusOK, "").Model(struct{ ID string }{}),
+	).Run(func(ctx Context) {
+		ctx.WriteHeader(http.StatusOK)
+	})
+
 	app.EnableGraphQL(&GraphQLConfig{
 		ComplexityLimit: 250,
+		IgnorePrefixes:  []string{"/ignored"},
 	})
 
 	query := strings.Replace(strings.Replace(`{
@@ -304,4 +311,22 @@ data:
 													id: target
 													url: https://www.target.com/
 `, "\t", "  ", -1), w.Body.String())
+
+	// Confirm expected top-level fields in the query API.
+	w = httptest.NewRecorder()
+	req, _ = http.NewRequest(http.MethodGet, "/graphql?query={__schema{queryType{fields{name}}}}", nil)
+	app.ServeHTTP(w, req)
+
+	assert.Equal(t, http.StatusOK, w.Code)
+	assert.YAMLEq(t, strings.Replace(`data:
+	__schema:
+		queryType:
+			fields:
+			- name: categories
+			- name: categoriesItem
+			- name: products
+			- name: productsItem
+			- name: stores
+			- name: storesItem
+`, "\t", "  ", -1), w.Body.String())
 }
Repository: danielgtaylor/huma
Pull request #65 (2022-09-12 17:41:16)

Fix GraphQL field casing by camel casing the path


Patch:
diff --git a/graphql.go b/graphql.go
index 0bd7763..af46ef8 100644
--- a/graphql.go
+++ b/graphql.go
@@ -182,6 +182,7 @@ func (r *Router) handleOperation(config *GraphQLConfig, parentName string, field
 		}
 		break
 	}
+	last = casing.LowerCamel(last)
 
 	// Setup input arguments (i.e. OpenAPI operation params).
 	args := graphql.FieldConfigArgument{}


Test patch:
diff --git a/graphql_test.go b/graphql_test.go
index fb5fc59..bab32ed 100644
--- a/graphql_test.go
+++ b/graphql_test.go
@@ -49,7 +49,12 @@ type StoreSummary struct {
 	ID string `json:"id" graphParam:"store-id" doc:"Store ID"`
 }
 
+type StoreName struct {
+	Name string `json:"name" graphParam:"store-name" doc:"Store Name"`
+}
+
 type Store struct {
+	StoreName
 	StoreSummary
 	URL string `json:"url" doc:"Web link to buy product"`
 }
@@ -57,8 +62,8 @@ type Store struct {
 func TestGraphQL(t *testing.T) {
 	now, _ := time.Parse(time.RFC3339, "2022-02-22T22:22:22Z")
 
-	amazon := &Store{StoreSummary: StoreSummary{ID: "amazon"}, URL: "https://www.amazon.com/"}
-	target := &Store{StoreSummary: StoreSummary{ID: "target"}, URL: "https://www.target.com/"}
+	amazon := &Store{StoreSummary: StoreSummary{ID: "amazon"}, URL: "https://www.amazon.com/", StoreName: StoreName{"amazon"}}
+	target := &Store{StoreSummary: StoreSummary{ID: "target"}, URL: "https://www.target.com/", StoreName: StoreName{"target"}}
 
 	xsx := &Product{ProductSummary: ProductSummary{ID: "xbox_series_x"}, SuggestedPrice: 499.99, Created: &now, Metadata: map[string]string{"foo": "bar"}, stores: map[string]*Store{"amazon": amazon, "target": target}, Empty: &struct{}{}}
 	ps5 := &Product{ProductSummary: ProductSummary{ID: "playstation_ps5"}, SuggestedPrice: 499.99, Created: &now, stores: map[string]*Store{"amazon": amazon}}
@@ -194,6 +199,21 @@ func TestGraphQL(t *testing.T) {
 		ctx.WriteModel(http.StatusOK, categories[input.CategoryID].products[input.ProductID].stores[input.StoreID])
 	})
 
+	app.Resource("/categories/{category-id}/products/{product-id}/stores/{store-id}/store-name").Get("get-store-name", "doc",
+		NewResponse(http.StatusOK, "").Model(&StoreName{}),
+		NewResponse(http.StatusNotFound, "").Model(&ErrorModel{}),
+	).Run(func(ctx Context, input struct {
+		CategoryParam
+		ProductParam
+		StoreID string `path:"store-id" doc:"Store ID"`
+	}) {
+		if categories[input.CategoryID] == nil || categories[input.CategoryID].products[input.ProductID] == nil {
+			ctx.WriteError(http.StatusNotFound, "Not found")
+			return
+		}
+		ctx.WriteModel(http.StatusOK, categories[input.CategoryID].products[input.ProductID].stores[input.StoreID].StoreName)
+	})
+
 	app.Resource("/ignored").Get("get-ignored", "doc",
 		NewResponse(http.StatusOK, "").Model(struct{ ID string }{}),
 	).Run(func(ctx Context) {
@@ -326,6 +346,7 @@ data:
 			- name: categoriesItem
 			- name: products
 			- name: productsItem
+			- name: storeName
 			- name: stores
 			- name: storesItem
 `, "\t", "  ", -1), w.Body.String())
Repository: danielgtaylor/huma
Pull request #67 (2022-09-13 23:21:02)

fix: panic on put without body
This adds an additional `nil` check to prevent a panic when registering or handling a request for a `PUT` operation without a request body. Also adds a test to exercise the functionality and confirm the auto-patch feature skips such operations.


Patch:
diff --git a/patch.go b/patch.go
index 731568a..e8c88be 100644
--- a/patch.go
+++ b/patch.go
@@ -81,7 +81,7 @@ func (r *Router) AutoPatch() {
 			case http.MethodPut:
 				put = op
 				_, reqDef := put.requestForContentType("application/json")
-				if reqDef.model != nil {
+				if reqDef != nil && reqDef.model != nil {
 					kind = reqDef.model.Kind()
 					if kind == reflect.Ptr {
 						kind = reqDef.model.Elem().Kind()
diff --git a/resolver.go b/resolver.go
index 0cc3d2b..f821cf8 100644
--- a/resolver.go
+++ b/resolver.go
@@ -237,7 +237,7 @@ func setFields(ctx *hcontext, req *http.Request, input reflect.Value, t reflect.
 				continue
 			}
 
-			if reqDef.schema != nil && reqDef.schema.HasValidation() {
+			if reqDef != nil && reqDef.schema != nil && reqDef.schema.HasValidation() {
 				if !validAgainstSchema(ctx, locationBody+".", reqDef.schema, data) {
 					continue
 				}


Test patch:
diff --git a/patch_test.go b/patch_test.go
index fe978b6..c7c3edc 100644
--- a/patch_test.go
+++ b/patch_test.go
@@ -170,3 +170,33 @@ func TestPatch(t *testing.T) {
 	app.ServeHTTP(w, req)
 	assert.Equal(t, http.StatusUnsupportedMediaType, w.Code, w.Body.String())
 }
+
+func TestPatchPutNoBody(t *testing.T) {
+	app := newTestRouter()
+
+	things := app.Resource("/things/{thing-id}")
+
+	things.Get("get-thing", "docs",
+		NewResponse(http.StatusOK, "OK").Model(&ThingModel{}),
+	).Run(func(ctx Context, input struct {
+		ThingIDParam
+	}) {
+		ctx.WriteModel(http.StatusOK, &ThingModel{})
+	})
+
+	things.Put("put-thing", "docs",
+		NewResponse(http.StatusNoContent, "No Content"),
+	).Run(func(ctx Context, input struct {
+		ThingIDParam
+		// Note: no body!
+	}) {
+		ctx.WriteHeader(http.StatusNoContent)
+	})
+
+	// There should be no generated PATCH since there is nothing to
+	// write in the PUT!
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest(http.MethodPatch, "/things/test", nil)
+	app.ServeHTTP(w, req)
+	assert.Equal(t, http.StatusMethodNotAllowed, w.Code, w.Body.String())
+}