github.com/xzntrc/go-enry/v2@v2.0.0-20230215091818-766cc1d65498/data/generated.go (about) 1 package data 2 3 import ( 4 "bytes" 5 "strings" 6 7 "github.com/go-enry/go-enry/v2/regex" 8 ) 9 10 // GeneratedCodeExtensions contains all extensions that belong to generated 11 // files for sure. 12 var GeneratedCodeExtensions = map[string]struct{}{ 13 // XCode files 14 ".nib": {}, 15 ".xcworkspacedata": {}, 16 ".xcuserstate": {}, 17 } 18 19 // GeneratedCodeNameMatcher is a function that tells whether the file with the 20 // given name is generated. 21 type GeneratedCodeNameMatcher func(string) bool 22 23 func nameMatches(pattern string) GeneratedCodeNameMatcher { 24 r := regex.MustCompile(pattern) 25 return func(name string) bool { 26 return r.MatchString(name) 27 } 28 } 29 30 func nameContains(pattern string) GeneratedCodeNameMatcher { 31 return func(name string) bool { 32 return strings.Contains(name, pattern) 33 } 34 } 35 36 func nameEndsWith(pattern string) GeneratedCodeNameMatcher { 37 return func(name string) bool { 38 return strings.HasSuffix(name, pattern) 39 } 40 } 41 42 // GeneratedCodeNameMatchers are all the matchers that check whether the code 43 // is generated based only on the file name. 44 var GeneratedCodeNameMatchers = []GeneratedCodeNameMatcher{ 45 // Cocoa pods 46 nameMatches(`(^Pods|\/Pods)\/`), 47 48 // Carthage build 49 nameMatches(`(^|\/)Carthage\/Build\/`), 50 51 // NET designer file 52 nameMatches(`(?i)\.designer\.(cs|vb)$`), 53 54 // Generated NET specflow feature file 55 nameEndsWith(".feature.cs"), 56 57 // Node modules 58 nameContains("node_modules/"), 59 60 // Go vendor 61 nameMatches(`vendor\/([-0-9A-Za-z]+\.)+(com|edu|gov|in|me|net|org|fm|io)`), 62 63 // Go lock 64 nameEndsWith("Gopkg.lock"), 65 nameEndsWith("glide.lock"), 66 67 // Esy lock 68 nameMatches(`(^|\/)(\w+\.)?esy.lock$`), 69 70 // NPM shrinkwrap 71 nameEndsWith("npm-shrinkwrap.json"), 72 73 // NPM package lock 74 nameEndsWith("package-lock.json"), 75 76 // Yarn plugnplay 77 nameMatches(`(^|\/)\.pnp\..*$`), 78 79 // Godeps 80 nameContains("Godeps/"), 81 82 // Composer lock 83 nameEndsWith("composer.lock"), 84 85 // Generated by zephir 86 nameMatches(`.\.zep\.(?:c|h|php)$`), 87 88 // Cargo lock 89 nameEndsWith("Cargo.lock"), 90 91 // Pipenv lock 92 nameEndsWith("Pipfile.lock"), 93 94 // GraphQL relay 95 nameContains("__generated__/"), 96 97 // Poetry lock 98 nameEndsWith("poetry.lock"), 99 } 100 101 // GeneratedCodeMatcher checks whether the file with the given data is 102 // generated code. 103 type GeneratedCodeMatcher func(path, ext string, content []byte) bool 104 105 // GeneratedCodeMatchers is the list of all generated code matchers that 106 // rely on checking the content of the file to make the guess. 107 var GeneratedCodeMatchers = []GeneratedCodeMatcher{ 108 isMinifiedFile, 109 hasSourceMapReference, 110 isSourceMap, 111 isCompiledCoffeeScript, 112 isGeneratedNetDocfile, 113 isGeneratedJavaScriptPEGParser, 114 isGeneratedPostScript, 115 isGeneratedGo, 116 isGeneratedProtobufFromGo, 117 isGeneratedProtobuf, 118 isGeneratedJavaScriptProtocolBuffer, 119 isGeneratedApacheThrift, 120 isGeneratedJNIHeader, 121 isVCRCassette, 122 isCompiledCythonFile, 123 isGeneratedModule, 124 isGeneratedUnity3DMeta, 125 isGeneratedRacc, 126 isGeneratedJFlex, 127 isGeneratedGrammarKit, 128 isGeneratedRoxygen2, 129 isGeneratedJison, 130 isGeneratedGRPCCpp, 131 isGeneratedDart, 132 isGeneratedPerlPPPortHeader, 133 isGeneratedGameMakerStudio, 134 isGeneratedGimp, 135 isGeneratedVisualStudio6, 136 isGeneratedHaxe, 137 isGeneratedHTML, 138 isGeneratedJooq, 139 } 140 141 func canBeMinified(ext string) bool { 142 return ext == ".js" || ext == ".css" 143 } 144 145 // isMinifiedFile returns whether the file may be minified. 146 // We consider a minified file any css or js file whose average number of chars 147 // per line is more than 110. 148 func isMinifiedFile(path, ext string, content []byte) bool { 149 if !canBeMinified(ext) { 150 return false 151 } 152 153 var chars, lines uint64 154 forEachLine(content, func(line []byte) { 155 chars += uint64(len(line)) 156 lines++ 157 }) 158 159 if lines == 0 { 160 return false 161 } 162 163 return chars/lines > 110 164 } 165 166 var sourceMapRegex = regex.MustCompile(`^\/[*\/][\#@] source(?:Mapping)?URL|sourceURL=`) 167 168 // hasSourceMapReference returns whether the file contains a reference to a 169 // source-map file. 170 func hasSourceMapReference(_ string, ext string, content []byte) bool { 171 if !canBeMinified(ext) { 172 return false 173 } 174 175 for _, line := range getLines(content, -2) { 176 if sourceMapRegex.Match(line) { 177 return true 178 } 179 } 180 181 return false 182 } 183 184 var sourceMapRegexps = []regex.EnryRegexp{ 185 regex.MustCompile(`^{"version":\d+,`), 186 regex.MustCompile(`^\/\*\* Begin line maps\. \*\*\/{`), 187 } 188 189 // isSourceMap returns whether the file itself is a source map. 190 func isSourceMap(path, _ string, content []byte) bool { 191 if strings.HasSuffix(path, ".js.map") || strings.HasSuffix(path, ".css.map") { 192 return true 193 } 194 195 firstLine := getFirstLine(content) 196 if len(firstLine) == 0 { 197 return false 198 } 199 200 for _, r := range sourceMapRegexps { 201 if r.Match(firstLine) { 202 return true 203 } 204 } 205 206 return false 207 } 208 209 func isCompiledCoffeeScript(path, ext string, content []byte) bool { 210 if ext != ".js" { 211 return false 212 } 213 214 firstLine := getFirstLine(content) 215 lastLines := getLines(content, -2) 216 if len(lastLines) < 2 { 217 return false 218 } 219 220 if string(firstLine) == "(function() {" && 221 string(lastLines[1]) == "}).call(this);" && 222 string(lastLines[0]) == "" { 223 score := 0 224 225 forEachLine(content, func(line []byte) { 226 if bytes.Contains(line, []byte("var ")) { 227 // Underscored temp vars are likely to be Coffee 228 score += 1 * countAppearancesInLine(line, "_fn", "_i", "_len", "_ref", "_results") 229 230 // bind and extend functions are very Coffee specific 231 score += 3 * countAppearancesInLine(line, "__bind", "__extends", "__hasProp", "__indexOf", "__slice") 232 } 233 }) 234 235 // Require a score of 3. This is fairly arbitrary. Consider tweaking later. 236 // See: https://github.com/github/linguist/blob/master/lib/linguist/generated.rb#L176-L213 237 return score >= 3 238 } 239 240 return false 241 } 242 243 func isGeneratedNetDocfile(_, ext string, content []byte) bool { 244 if ext != ".xml" { 245 return false 246 } 247 248 lines := bytes.Split(content, []byte{'\n'}) 249 if len(lines) <= 3 { 250 return false 251 } 252 253 return bytes.Contains(lines[1], []byte("<doc>")) && 254 bytes.Contains(lines[2], []byte("<assembly>")) && 255 bytes.Contains(lines[len(lines)-2], []byte("</doc>")) 256 } 257 258 var pegJavaScriptGeneratedRegex = regex.MustCompile(`^(?:[^\/]|\/[^\*])*\/\*(?:[^\*]|\*[^\/])*Generated by PEG.js`) 259 260 func isGeneratedJavaScriptPEGParser(_, ext string, content []byte) bool { 261 if ext != ".js" { 262 return false 263 } 264 265 // PEG.js-generated parsers include a comment near the top of the file 266 // that marks them as such. 267 return pegJavaScriptGeneratedRegex.Match(bytes.Join(getLines(content, 5), []byte(""))) 268 } 269 270 var postScriptType1And42Regex = regex.MustCompile(`(\n|\r\n|\r)\s*(?:currentfile eexec\s+|\/sfnts\s+\[)`) 271 272 var postScriptRegexes = []regex.EnryRegexp{ 273 regex.MustCompile(`[0-9]|draw|mpage|ImageMagick|inkscape|MATLAB`), 274 regex.MustCompile(`PCBNEW|pnmtops|\(Unknown\)|Serif Affinity|Filterimage -tops`), 275 } 276 277 func isGeneratedPostScript(_, ext string, content []byte) bool { 278 if ext != ".ps" && ext != ".eps" && ext != ".pfa" { 279 return false 280 } 281 282 // Type 1 and Type 42 fonts converted to PostScript are stored as hex-encoded byte streams; these 283 // streams are always preceded the `eexec` operator (if Type 1), or the `/sfnts` key (if Type 42). 284 if postScriptType1And42Regex.Match(content) { 285 return true 286 } 287 288 // We analyze the "%%Creator:" comment, which contains the author/generator 289 // of the file. If there is one, it should be in one of the first few lines. 290 var creator []byte 291 for _, line := range getLines(content, 10) { 292 if bytes.HasPrefix(line, []byte("%%Creator: ")) { 293 creator = line 294 break 295 } 296 } 297 298 if len(creator) == 0 { 299 return false 300 } 301 302 // EAGLE doesn't include a version number when it generates PostScript. 303 // However, it does prepend its name to the document's "%%Title" field. 304 if bytes.Contains(creator, []byte("EAGLE")) { 305 for _, line := range getLines(content, 5) { 306 if bytes.HasPrefix(line, []byte("%%Title: EAGLE Drawing ")) { 307 return true 308 } 309 } 310 } 311 312 // Most generators write their version number, while human authors' or companies' 313 // names don't contain numbers. So look if the line contains digits. Also 314 // look for some special cases without version numbers. 315 for _, r := range postScriptRegexes { 316 if r.Match(creator) { 317 return true 318 } 319 } 320 321 return false 322 } 323 324 func isGeneratedGo(_, ext string, content []byte) bool { 325 if ext != ".go" { 326 return false 327 } 328 329 lines := getLines(content, 40) 330 if len(lines) <= 1 { 331 return false 332 } 333 334 for _, line := range lines { 335 if bytes.Contains(line, []byte("Code generated by")) { 336 return true 337 } 338 } 339 340 return false 341 } 342 343 func isGeneratedProtobufFromGo(_, ext string, content []byte) bool { 344 if ext != ".proto" { 345 return false 346 } 347 lines := getLines(content, 20) 348 if len(lines) <= 1 { 349 return false 350 } 351 352 for _, line := range lines { 353 if bytes.Contains(line, []byte("This file was autogenerated by go-to-protobuf")) { 354 return true 355 } 356 } 357 358 return false 359 } 360 361 var protoExtensions = map[string]struct{}{ 362 ".py": {}, 363 ".java": {}, 364 ".h": {}, 365 ".cc": {}, 366 ".cpp": {}, 367 ".m": {}, 368 ".rb": {}, 369 ".php": {}, 370 } 371 372 func isGeneratedProtobuf(_, ext string, content []byte) bool { 373 if _, ok := protoExtensions[ext]; !ok { 374 return false 375 } 376 377 lines := getLines(content, 3) 378 if len(lines) <= 1 { 379 return false 380 } 381 382 for _, line := range lines { 383 if bytes.Contains(line, []byte("Generated by the protocol buffer compiler. DO NOT EDIT!")) { 384 return true 385 } 386 } 387 388 return false 389 } 390 391 func isGeneratedJavaScriptProtocolBuffer(_, ext string, content []byte) bool { 392 if ext != ".js" { 393 return false 394 } 395 396 lines := getLines(content, 6) 397 if len(lines) < 6 { 398 return false 399 } 400 401 return bytes.Contains(lines[5], []byte("GENERATED CODE -- DO NOT EDIT!")) 402 } 403 404 var apacheThriftExtensions = map[string]struct{}{ 405 ".rb": {}, 406 ".py": {}, 407 ".go": {}, 408 ".js": {}, 409 ".m": {}, 410 ".java": {}, 411 ".h": {}, 412 ".cc": {}, 413 ".cpp": {}, 414 ".php": {}, 415 } 416 417 func isGeneratedApacheThrift(_, ext string, content []byte) bool { 418 if _, ok := apacheThriftExtensions[ext]; !ok { 419 return false 420 } 421 422 for _, line := range getLines(content, 6) { 423 if bytes.Contains(line, []byte("Autogenerated by Thrift Compiler")) { 424 return true 425 } 426 } 427 428 return false 429 } 430 431 func isGeneratedJNIHeader(_, ext string, content []byte) bool { 432 if ext != ".h" { 433 return false 434 } 435 436 lines := getLines(content, 2) 437 if len(lines) < 2 { 438 return false 439 } 440 441 return bytes.Contains(lines[0], []byte("/* DO NOT EDIT THIS FILE - it is machine generated */")) && 442 bytes.Contains(lines[1], []byte("#include <jni.h>")) 443 } 444 445 func isVCRCassette(_, ext string, content []byte) bool { 446 if ext != ".yml" { 447 return false 448 } 449 450 lines := getLines(content, -2) 451 if len(lines) < 2 { 452 return false 453 } 454 455 return bytes.Contains(lines[1], []byte("recorded_with: VCR")) 456 } 457 458 func isCompiledCythonFile(_, ext string, content []byte) bool { 459 if ext != ".c" && ext != ".cpp" { 460 return false 461 } 462 463 lines := getLines(content, 1) 464 if len(lines) < 1 { 465 return false 466 } 467 468 return bytes.Contains(lines[0], []byte("Generated by Cython")) 469 } 470 471 func isGeneratedModule(_, ext string, content []byte) bool { 472 if ext != ".mod" { 473 return false 474 } 475 476 lines := getLines(content, 1) 477 if len(lines) < 1 { 478 return false 479 } 480 481 return bytes.Contains(lines[0], []byte("PCBNEW-LibModule-V")) || 482 bytes.Contains(lines[0], []byte("GFORTRAN module version '")) 483 } 484 485 func isGeneratedUnity3DMeta(_, ext string, content []byte) bool { 486 if ext != ".meta" { 487 return false 488 } 489 490 lines := getLines(content, 1) 491 if len(lines) < 1 { 492 return false 493 } 494 495 return bytes.Contains(lines[0], []byte("fileFormatVersion: ")) 496 } 497 498 func isGeneratedRacc(_, ext string, content []byte) bool { 499 if ext != ".rb" { 500 return false 501 } 502 503 lines := getLines(content, 3) 504 if len(lines) < 3 { 505 return false 506 } 507 508 return bytes.HasPrefix(lines[2], []byte("# This file is automatically generated by Racc")) 509 } 510 511 func isGeneratedJFlex(_, ext string, content []byte) bool { 512 if ext != ".java" { 513 return false 514 } 515 516 lines := getLines(content, 1) 517 if len(lines) < 1 { 518 return false 519 } 520 521 return bytes.HasPrefix(lines[0], []byte("/* The following code was generated by JFlex ")) 522 } 523 524 func isGeneratedGrammarKit(_, ext string, content []byte) bool { 525 if ext != ".java" { 526 return false 527 } 528 529 lines := getLines(content, 1) 530 if len(lines) < 1 { 531 return false 532 } 533 534 return bytes.Contains(lines[0], []byte("// This is a generated file. Not intended for manual editing.")) 535 } 536 537 func isGeneratedRoxygen2(_, ext string, content []byte) bool { 538 if ext != ".rd" { 539 return false 540 } 541 542 lines := getLines(content, 1) 543 if len(lines) < 1 { 544 return false 545 } 546 547 return bytes.Contains(lines[0], []byte("% Generated by roxygen2: do not edit by hand")) 548 } 549 550 func isGeneratedJison(_, ext string, content []byte) bool { 551 if ext != ".js" { 552 return false 553 } 554 555 lines := getLines(content, 1) 556 if len(lines) < 1 { 557 return false 558 } 559 560 return bytes.Contains(lines[0], []byte("/* parser generated by jison ")) || 561 bytes.Contains(lines[0], []byte("/* generated by jison-lex ")) 562 } 563 564 func isGeneratedGRPCCpp(_, ext string, content []byte) bool { 565 switch ext { 566 case ".cpp", ".hpp", ".h", ".cc": 567 lines := getLines(content, 1) 568 if len(lines) < 1 { 569 return false 570 } 571 572 return bytes.Contains(lines[0], []byte("// Generated by the gRPC")) 573 default: 574 return false 575 } 576 } 577 578 var dartRegex = regex.MustCompile(`generated code\W{2,3}do not modify`) 579 580 func isGeneratedDart(_, ext string, content []byte) bool { 581 if ext != ".dart" { 582 return false 583 } 584 585 lines := getLines(content, 1) 586 if len(lines) < 1 { 587 return false 588 } 589 590 return dartRegex.Match(bytes.ToLower(lines[0])) 591 } 592 593 func isGeneratedPerlPPPortHeader(name, _ string, content []byte) bool { 594 if !strings.HasSuffix(name, "ppport.h") { 595 return false 596 } 597 598 lines := getLines(content, 10) 599 if len(lines) < 10 { 600 return false 601 } 602 603 return bytes.Contains(lines[8], []byte("Automatically created by Devel::PPPort")) 604 } 605 606 var ( 607 gameMakerStudioFirstLineRegex = regex.MustCompile(`^\d\.\d\.\d.+\|\{`) 608 gameMakerStudioThirdLineRegex = regex.MustCompile(`\"modelName\"\:\s*\"GM`) 609 ) 610 611 func isGeneratedGameMakerStudio(_, ext string, content []byte) bool { 612 if ext != ".yy" && ext != ".yyp" { 613 return false 614 } 615 616 lines := getLines(content, 3) 617 if len(lines) < 3 { 618 return false 619 } 620 621 return gameMakerStudioThirdLineRegex.Match(lines[2]) || 622 gameMakerStudioFirstLineRegex.Match(lines[0]) 623 } 624 625 var gimpRegexes = []regex.EnryRegexp{ 626 regex.MustCompile(`\/\* GIMP [a-zA-Z0-9\- ]+ C\-Source image dump \(.+?\.c\) \*\/`), 627 regex.MustCompile(`\/\* GIMP header image file format \([a-zA-Z0-9\- ]+\)\: .+?\.h \*\/`), 628 } 629 630 func isGeneratedGimp(_, ext string, content []byte) bool { 631 if ext != ".c" && ext != ".h" { 632 return false 633 } 634 635 lines := getLines(content, 1) 636 if len(lines) < 1 { 637 return false 638 } 639 640 for _, r := range gimpRegexes { 641 if r.Match(lines[0]) { 642 return true 643 } 644 } 645 646 return false 647 } 648 649 func isGeneratedVisualStudio6(_, ext string, content []byte) bool { 650 if ext != ".dsp" { 651 return false 652 } 653 654 for _, l := range getLines(content, 3) { 655 if bytes.Contains(l, []byte("# Microsoft Developer Studio Generated Build File")) { 656 return true 657 } 658 } 659 660 return false 661 } 662 663 var haxeExtensions = map[string]struct{}{ 664 ".js": {}, 665 ".py": {}, 666 ".lua": {}, 667 ".cpp": {}, 668 ".h": {}, 669 ".java": {}, 670 ".cs": {}, 671 ".php": {}, 672 } 673 674 func isGeneratedHaxe(_, ext string, content []byte) bool { 675 if _, ok := haxeExtensions[ext]; !ok { 676 return false 677 } 678 679 for _, l := range getLines(content, 3) { 680 if bytes.Contains(l, []byte("Generated by Haxe")) { 681 return true 682 } 683 } 684 685 return false 686 } 687 688 var ( 689 doxygenRegex = regex.MustCompile(`<!--\s+Generated by Doxygen\s+[.0-9]+\s*-->`) 690 htmlMetaRegex = regex.MustCompile(`<meta(\s+[^>]+)>`) 691 htmlMetaContentRegex = regex.MustCompile(`\s+(name|content|value)\s*=\s*("[^"]+"|'[^']+'|[^\s"']+)`) 692 orgModeMetaRegex = regex.MustCompile(`org\s+mode`) 693 ) 694 695 func isGeneratedHTML(_, ext string, content []byte) bool { 696 if ext != ".html" && ext != ".htm" && ext != ".xhtml" { 697 return false 698 } 699 700 lines := getLines(content, 30) 701 702 // Pkgdown 703 if len(lines) >= 2 { 704 for _, l := range lines[:2] { 705 if bytes.Contains(l, []byte("<!-- Generated by pkgdown: do not edit by hand -->")) { 706 return true 707 } 708 } 709 } 710 711 // Mandoc 712 if len(lines) > 2 && 713 bytes.HasPrefix(lines[2], []byte("<!-- This is an automatically generated file.")) { 714 return true 715 } 716 717 // Doxygen 718 for _, l := range lines { 719 if doxygenRegex.Match(l) { 720 return true 721 } 722 } 723 724 // HTML tag: <meta name="generator" content="" /> 725 part := bytes.ToLower(bytes.Join(lines, []byte{' '})) 726 part = bytes.ReplaceAll(part, []byte{'\n'}, []byte{}) 727 part = bytes.ReplaceAll(part, []byte{'\r'}, []byte{}) 728 matches := htmlMetaRegex.FindAll(part, -1) 729 if len(matches) == 0 { 730 return false 731 } 732 733 for _, m := range matches { 734 var name, value, content string 735 ms := htmlMetaContentRegex.FindAllStringSubmatch(string(m), -1) 736 for _, m := range ms { 737 switch m[1] { 738 case "name": 739 name = m[2] 740 case "value": 741 value = m[2] 742 case "content": 743 content = m[2] 744 } 745 } 746 747 var val = value 748 if val == "" { 749 val = content 750 } 751 752 name = strings.Trim(name, `"'`) 753 val = strings.Trim(val, `"'`) 754 755 if name != "generator" || val == "" { 756 continue 757 } 758 759 if strings.Contains(val, "jlatex2html") || 760 strings.Contains(val, "latex2html") || 761 strings.Contains(val, "groff") || 762 strings.Contains(val, "makeinfo") || 763 strings.Contains(val, "texi2html") || 764 strings.Contains(val, "ronn") || 765 orgModeMetaRegex.MatchString(val) { 766 return true 767 } 768 } 769 770 return false 771 } 772 773 func isGeneratedJooq(_, ext string, content []byte) bool { 774 if ext != ".java" { 775 return false 776 } 777 778 for _, l := range getLines(content, 2) { 779 if bytes.Contains(l, []byte("This file is generated by jOOQ.")) { 780 return true 781 } 782 } 783 784 return false 785 } 786 787 func getFirstLine(content []byte) []byte { 788 lines := getLines(content, 1) 789 if len(lines) > 0 { 790 return lines[0] 791 } 792 return nil 793 } 794 795 // getLines returns up to the first n lines. A negative index will return up to 796 // the last n lines in reverse order. 797 func getLines(content []byte, n int) [][]byte { 798 var result [][]byte 799 if n < 0 { 800 for pos := len(content); pos > 0 && len(result) < -n; { 801 nlpos := bytes.LastIndexByte(content[:pos], '\n') 802 if nlpos+1 < len(content)-1 { 803 result = append(result, content[nlpos+1:pos]) 804 } 805 pos = nlpos 806 } 807 } else { 808 for pos := 0; pos < len(content) && len(result) < n; { 809 nlpos := bytes.IndexByte(content[pos:], '\n') 810 if nlpos < 0 && pos < len(content) { 811 nlpos = len(content) 812 } else if nlpos >= 0 { 813 nlpos += pos 814 } 815 816 result = append(result, content[pos:nlpos]) 817 pos = nlpos + 1 818 } 819 } 820 821 return result 822 } 823 824 func forEachLine(content []byte, cb func([]byte)) { 825 var pos int 826 for pos < len(content) { 827 nlpos := bytes.IndexByte(content[pos:], '\n') 828 if nlpos < 0 && pos < len(content) { 829 nlpos = len(content) 830 } else if nlpos >= 0 { 831 nlpos += pos 832 } 833 834 cb(content[pos:nlpos]) 835 pos = nlpos + 1 836 } 837 } 838 839 func countAppearancesInLine(line []byte, targets ...string) int { 840 var count int 841 for _, t := range targets { 842 count += bytes.Count(line, []byte(t)) 843 } 844 return count 845 }