@@ -217,30 +217,109 @@ var knownFieldValidValues = map[string]string{
217217 "/permissions" : "Valid permission scopes: actions, all, attestations, checks, contents, deployments, discussions, id-token, issues, metadata, models, organization-projects, packages, pages, pull-requests, repository-projects, security-events, statuses, vulnerability-alerts" ,
218218}
219219
220- // appendKnownFieldValidValuesHint appends a "Valid values: …" hint to message when the
221- // jsonPath matches a well-known field and the message is an unknown-property error.
220+ // knownFieldScopes maps well-known JSON schema paths to a slice of valid scope names.
221+ // This enables spell-check ("Did you mean?") suggestions for unknown-property errors.
222+ //
223+ // The permissions scope list mirrors permissions.oneOf[1].properties in main_workflow_schema.json.
224+ // Update both when the schema changes.
225+ var knownFieldScopes = map [string ][]string {
226+ "/permissions" : {
227+ "actions" , "all" , "attestations" , "checks" , "contents" , "deployments" ,
228+ "discussions" , "id-token" , "issues" , "metadata" , "models" ,
229+ "organization-projects" , "packages" , "pages" , "pull-requests" ,
230+ "repository-projects" , "security-events" , "statuses" , "vulnerability-alerts" ,
231+ },
232+ }
233+
234+ // knownFieldDocs maps well-known JSON schema paths to documentation URLs.
235+ var knownFieldDocs = map [string ]string {
236+ "/permissions" : "https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/controlling-permissions-for-github_token" ,
237+ }
238+
239+ // unknownPropertyPattern extracts the property name(s) from a rewritten "Unknown property(ies):" message.
240+ var unknownPropertyPattern = regexp .MustCompile (`(?i)^Unknown propert(?:y|ies): (.+)$` )
241+
242+ // appendKnownFieldValidValuesHint appends a "Valid values: …" hint, "Did you mean?" suggestions,
243+ // and a documentation link to message when the jsonPath matches a well-known field and the
244+ // message is an unknown-property error.
222245// It returns the message unchanged for unknown paths or non-additional-properties messages.
223246func appendKnownFieldValidValuesHint (message string , jsonPath string ) string {
224247 // Use truncated prefix "unknown propert" to match both singular ("Unknown property")
225248 // and plural ("Unknown properties") forms produced by rewriteAdditionalPropertiesError.
226249 if ! strings .Contains (strings .ToLower (message ), "unknown propert" ) {
227250 return message
228251 }
229- hint , ok := knownFieldValidValues [jsonPath ]
230- if ! ok {
231- // Check if the path is nested under a known parent (e.g. /permissions/contents)
232- for path , h := range knownFieldValidValues {
252+
253+ // Find the best matching known path: exact match first, then the longest matching parent.
254+ hint , hintOK := knownFieldValidValues [jsonPath ]
255+ scopes := knownFieldScopes [jsonPath ]
256+ docsURL := knownFieldDocs [jsonPath ]
257+ if ! hintOK {
258+ // Select the longest matching parent path deterministically to avoid
259+ // random map iteration order when multiple known paths share a common prefix.
260+ bestPath := ""
261+ bestLen := 0
262+ for path := range knownFieldValidValues {
233263 if strings .HasPrefix (jsonPath , path + "/" ) {
234- hint = h
235- ok = true
236- break
264+ if l := len (path ); l > bestLen {
265+ bestLen = l
266+ bestPath = path
267+ }
237268 }
238269 }
270+ if bestPath != "" {
271+ hint = knownFieldValidValues [bestPath ]
272+ scopes = knownFieldScopes [bestPath ]
273+ docsURL = knownFieldDocs [bestPath ]
274+ hintOK = true
275+ }
239276 }
240- if ! ok {
277+ if ! hintOK {
241278 return message
242279 }
243- return message + " (" + hint + ")"
280+
281+ result := message + " (" + hint + ")"
282+
283+ // Add "Did you mean?" suggestions when the unknown property name is close to a valid scope.
284+ if len (scopes ) > 0 {
285+ // unknownPropertyPattern has exactly one capture group, so a successful match
286+ // returns [fullMatch, captureGroup1], giving len(m) == 2.
287+ if m := unknownPropertyPattern .FindStringSubmatch (message ); len (m ) == 2 {
288+ unknownProps := strings .Split (m [1 ], ", " )
289+ var allSuggestions []string
290+ for _ , prop := range unknownProps {
291+ prop = strings .TrimSpace (prop )
292+ if prop == "" {
293+ continue
294+ }
295+ // maxClosestMatches is defined in schema_suggestions.go in the same package.
296+ closest := FindClosestMatches (prop , scopes , maxClosestMatches )
297+ allSuggestions = append (allSuggestions , closest ... )
298+ }
299+ // Deduplicate suggestions
300+ seen := make (map [string ]bool )
301+ var unique []string
302+ for _ , s := range allSuggestions {
303+ if ! seen [s ] {
304+ seen [s ] = true
305+ unique = append (unique , s )
306+ }
307+ }
308+ if len (unique ) == 1 {
309+ result = fmt .Sprintf ("%s. Did you mean '%s'?" , result , unique [0 ])
310+ } else if len (unique ) > 1 {
311+ result = fmt .Sprintf ("%s. Did you mean: %s?" , result , strings .Join (unique , ", " ))
312+ }
313+ }
314+ }
315+
316+ // Append documentation link on the same line to avoid breaking bullet-list formatting
317+ // when this message is embedded in "Multiple schema validation failures:" output.
318+ if docsURL != "" {
319+ result = fmt .Sprintf ("%s See: %s" , result , docsURL )
320+ }
321+
322+ return result
244323}
245324
246325// rewriteAdditionalPropertiesError rewrites "additional properties not allowed" errors to be more user-friendly
0 commit comments