this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

basic test setup for lex linting

+257 -20
+91 -20
cmd/glot/lexlint/lint.go
··· 1 1 package lexlint 2 2 3 3 import ( 4 - "errors" 5 4 "fmt" 6 5 "log/slog" 7 6 "slices" 7 + "strings" 8 8 9 9 "github.com/bluesky-social/indigo/atproto/lexicon" 10 10 "github.com/bluesky-social/indigo/atproto/syntax" 11 - ) 12 - 13 - var ( 14 - // internal error used to set non-zero return code (but not print separately) 15 - ErrLintFailures = errors.New("linting issues detected") 16 11 ) 17 12 18 13 type LintIssue struct { ··· 106 101 } 107 102 108 103 switch def.Inner.(type) { 109 - case lexicon.SchemaRecord, lexicon.SchemaQuery, lexicon.SchemaProcedure, lexicon.SchemaSubscription, lexicon.SchemaPermissionSet: 104 + // NOTE: not requiring description on permission-set 105 + case lexicon.SchemaRecord, lexicon.SchemaQuery, lexicon.SchemaProcedure, lexicon.SchemaSubscription: 110 106 if defname != "main" { 111 107 issues = append(issues, LintIssue{ 112 108 NSID: nsid, ··· 249 245 issues := []LintIssue{} 250 246 251 247 switch v := def.Inner.(type) { 252 - case lexicon.SchemaPermission: 253 - // TODO: anything? 254 248 case lexicon.SchemaBoolean: 255 249 // TODO: default true 256 250 // TODO: both default and const 257 251 case lexicon.SchemaInteger: 258 252 // TODO: both default and const 259 253 case lexicon.SchemaString: 260 - // TODO: no format and no max length 261 254 // TODO: format and length limits 262 255 // TODO: grapheme limit set, and maxlen either too low or not set 263 - // TODO: very large max size 264 256 // TODO: format=handle strings within an record type 257 + if v.MaxLength != nil && *v.MaxLength > 20*1024 { 258 + issues = append(issues, LintIssue{ 259 + NSID: nsid, 260 + LintLevel: "warn", 261 + LintName: "large-string", 262 + LintDescription: "string field with large maximum size (use blobs instead)", 263 + Message: "large max length", 264 + }) 265 + } 266 + if v.Format == nil && v.MaxLength == nil && v.MaxGraphemes == nil { 267 + issues = append(issues, LintIssue{ 268 + NSID: nsid, 269 + LintLevel: "warn", 270 + LintName: "unlimited-string", 271 + LintDescription: "string field with no format or maximum size", 272 + Message: "no max length", 273 + }) 274 + } 275 + for _, val := range v.KnownValues { 276 + if strings.HasPrefix(val, "#") { 277 + issues = append(issues, LintIssue{ 278 + NSID: nsid, 279 + LintLevel: "warn", 280 + LintName: "known-string-local-ref", 281 + LintDescription: "string knownValues entry which seems to be a local reference", 282 + Message: fmt.Sprintf("possible local ref: %s", val), 283 + }) 284 + } 285 + } 265 286 case lexicon.SchemaBytes: 266 - // TODO: very large max size 287 + if v.MaxLength == nil { 288 + issues = append(issues, LintIssue{ 289 + NSID: nsid, 290 + LintLevel: "warn", 291 + LintName: "unlimited-bytes", 292 + LintDescription: "bytes field with no maximum size", 293 + Message: "no max length", 294 + }) 295 + } 296 + if v.MaxLength != nil && *v.MaxLength > 20*1024 { 297 + // TODO: limit this to 'record' schemas? 298 + issues = append(issues, LintIssue{ 299 + NSID: nsid, 300 + LintLevel: "warn", 301 + LintName: "large-bytes", 302 + LintDescription: "bytes field with large maximum size (use blobs instead)", 303 + Message: "large max length", 304 + }) 305 + } 267 306 case lexicon.SchemaCIDLink: 268 307 // pass 308 + case lexicon.SchemaBlob: 309 + // pass 269 310 case lexicon.SchemaArray: 270 311 reciss := lintSchemaRecursive(nsid, v.Items) 271 312 if len(reciss) > 0 { ··· 273 314 } 274 315 case lexicon.SchemaObject: 275 316 // NOTE: CheckSchema already verifies that nullable and required are valid against property keys 276 - for _, propdef := range v.Properties { 317 + for fieldName, propdef := range v.Properties { 277 318 reciss := lintSchemaRecursive(nsid, propdef) 278 319 if len(reciss) > 0 { 279 320 issues = append(issues, reciss...) 280 321 } 281 - // TODO: property name syntax 322 + if err := CheckSchemaName(fieldName); err != nil { 323 + issues = append(issues, LintIssue{ 324 + NSID: nsid, 325 + LintLevel: "warn", 326 + LintName: "field-name-syntax", 327 + LintDescription: "field name does not follow syntax guidance", 328 + Message: fmt.Sprintf("%s: %s", err.Error(), fieldName), 329 + }) 330 + } 282 331 } 283 332 for _, k := range v.Nullable { 284 333 if !slices.Contains(v.Required, k) { ··· 291 340 }) 292 341 } 293 342 } 294 - case lexicon.SchemaBlob: 295 - // pass 296 343 case lexicon.SchemaParams: 297 344 // NOTE: CheckSchema already verifies that required are valid against property keys 298 - for _, propdef := range v.Properties { 345 + for fieldName, propdef := range v.Properties { 299 346 reciss := lintSchemaRecursive(nsid, propdef) 300 347 if len(reciss) > 0 { 301 348 issues = append(issues, reciss...) 302 349 } 303 - // TODO: property name syntax 350 + if err := CheckSchemaName(fieldName); err != nil { 351 + issues = append(issues, LintIssue{ 352 + NSID: nsid, 353 + LintLevel: "warn", 354 + LintName: "field-name-syntax", 355 + LintDescription: "field name does not follow syntax guidance", 356 + Message: fmt.Sprintf("%s: %s", err.Error(), fieldName), 357 + }) 358 + } 304 359 } 305 360 case lexicon.SchemaToken: 306 - // pass 361 + if v.Description == nil || *v.Description == "" { 362 + issues = append(issues, LintIssue{ 363 + NSID: nsid, 364 + LintLevel: "warn", 365 + LintName: "undescribed-token", 366 + LintDescription: "token without description field", 367 + Message: "empty description", 368 + }) 369 + } 307 370 case lexicon.SchemaRef: 308 371 // TODO: resolve? locally vs globally? 309 372 case lexicon.SchemaUnion: ··· 319 382 issues = append(issues, reciss...) 320 383 } 321 384 } 322 - // TODO: empty/invalid Encoding (mimetype) 385 + if v.Encoding == "" { 386 + issues = append(issues, LintIssue{ 387 + NSID: nsid, 388 + LintLevel: "warn", 389 + LintName: "unspecified-encoding", 390 + LintDescription: "body encoding not specified", 391 + Message: "missing encoding", 392 + }) 393 + } 323 394 default: 324 395 slog.Info("no lint rules for recursive schema type", "type", fmt.Sprintf("%T", def.Inner), "nsid", nsid) 325 396 }
+54
cmd/glot/lexlint/lint_test.go
··· 1 + package lexlint 2 + 3 + import ( 4 + "encoding/json" 5 + "io" 6 + "os" 7 + "sort" 8 + "testing" 9 + 10 + "github.com/bluesky-social/indigo/atproto/lexicon" 11 + 12 + "github.com/stretchr/testify/assert" 13 + ) 14 + 15 + type LintFixture struct { 16 + Name string `json:"name"` 17 + Schema lexicon.SchemaFile `json:"schema"` 18 + Issues []string `json:"issues"` 19 + } 20 + 21 + func TestLexLintFixtures(t *testing.T) { 22 + assert := assert.New(t) 23 + 24 + f, err := os.Open("testdata/lint-examples.json") 25 + if err != nil { 26 + t.Fatal(err) 27 + } 28 + defer func() { _ = f.Close() }() 29 + 30 + jsonBytes, err := io.ReadAll(f) 31 + if err != nil { 32 + t.Fatal(err) 33 + } 34 + 35 + var fixtures []LintFixture 36 + if err := json.Unmarshal(jsonBytes, &fixtures); err != nil { 37 + t.Fatal(err) 38 + } 39 + 40 + for _, f := range fixtures { 41 + assert.NoError(f.Schema.FinishParse()) 42 + 43 + issues := LintSchemaFile(&f.Schema) 44 + 45 + found := []string{} 46 + for _, iss := range issues { 47 + found = append(found, iss.LintName) 48 + } 49 + 50 + sort.Strings(found) 51 + sort.Strings(f.Issues) 52 + assert.Equal(f.Issues, found, f.Name) 53 + } 54 + }
+36
cmd/glot/lexlint/syntax_test.go
··· 1 + package lexlint 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/stretchr/testify/assert" 7 + ) 8 + 9 + func TestCheckSchemaName(t *testing.T) { 10 + assert := assert.New(t) 11 + 12 + goodNames := []string{ 13 + "blahFunc", 14 + "blahFuncV2", 15 + } 16 + 17 + badNames := []string{ 18 + "", 19 + " ", 20 + "blah Func", 21 + "blah_Func", 22 + "blah-Func", 23 + " blahFunc", 24 + "one.two", 25 + ".", 26 + "2blahFunc", 27 + } 28 + 29 + for _, name := range goodNames { 30 + assert.NoError(CheckSchemaName(name)) 31 + } 32 + 33 + for _, name := range badNames { 34 + assert.Error(CheckSchemaName(name)) 35 + } 36 + }
+76
cmd/glot/lexlint/testdata/lint-examples.json
··· 1 + [ 2 + { 3 + "name": "minimal, no lint problems", 4 + "schema": { 5 + "lexicon": 1, 6 + "id": "example.lexicon.record", 7 + "defs": { 8 + "main": { 9 + "type": "record", 10 + "description": "minimal description", 11 + "key": "any", 12 + "record": { 13 + "type": "object", 14 + "properties": {} 15 + } 16 + } 17 + } 18 + }, 19 + "issues": [] 20 + }, 21 + { 22 + "name": "bad field name", 23 + "schema": { 24 + "lexicon": 1, 25 + "id": "example.lexicon.record", 26 + "defs": { 27 + "main": { 28 + "type": "record", 29 + "description": "minimal description", 30 + "key": "any", 31 + "record": { 32 + "type": "object", 33 + "properties": { 34 + "one.two": { 35 + "type": "string", 36 + "maxLength": 20 37 + } 38 + } 39 + } 40 + } 41 + } 42 + }, 43 + "issues": ["field-name-syntax"] 44 + }, 45 + { 46 + "name": "local ref in string known values", 47 + "schema": { 48 + "lexicon": 1, 49 + "id": "example.lexicon.record", 50 + "defs": { 51 + "main": { 52 + "type": "record", 53 + "description": "minimal description", 54 + "key": "any", 55 + "record": { 56 + "type": "object", 57 + "properties": { 58 + "strField": { 59 + "type": "string", 60 + "maxLength": 100, 61 + "knownValues": [ 62 + "#green" 63 + ] 64 + } 65 + } 66 + } 67 + }, 68 + "green": { 69 + "type": "token", 70 + "description": "asdf" 71 + } 72 + } 73 + }, 74 + "issues": ["known-string-local-ref"] 75 + } 76 + ]