github.com/evanw/esbuild@v0.21.4/internal/logger/logger.go (about) 1 package logger 2 3 // Logging is either done to stderr (via "NewStderrLog") or to an in-memory 4 // array (via "NewDeferLog"). In-memory arrays are used to capture messages 5 // from parsing individual files because during incremental builds, log 6 // messages for a given file can be replayed from memory if the file ends up 7 // not being reparsed. 8 // 9 // Errors are streamed asynchronously as they happen, each error contains the 10 // contents of the line with the error, and the error count is limited by 11 // default. 12 13 import ( 14 "encoding/binary" 15 "fmt" 16 "os" 17 "runtime" 18 "sort" 19 "strings" 20 "sync" 21 "time" 22 "unicode/utf8" 23 ) 24 25 const defaultTerminalWidth = 80 26 27 type Log struct { 28 AddMsg func(Msg) 29 HasErrors func() bool 30 Peek func() []Msg 31 32 Done func() []Msg 33 34 Level LogLevel 35 Overrides map[MsgID]LogLevel 36 } 37 38 type LogLevel int8 39 40 const ( 41 LevelNone LogLevel = iota 42 LevelVerbose 43 LevelDebug 44 LevelInfo 45 LevelWarning 46 LevelError 47 LevelSilent 48 ) 49 50 type MsgKind uint8 51 52 const ( 53 Error MsgKind = iota 54 Warning 55 Info 56 Note 57 Debug 58 Verbose 59 ) 60 61 func (kind MsgKind) String() string { 62 switch kind { 63 case Error: 64 return "ERROR" 65 case Warning: 66 return "WARNING" 67 case Info: 68 return "INFO" 69 case Note: 70 return "NOTE" 71 case Debug: 72 return "DEBUG" 73 case Verbose: 74 return "VERBOSE" 75 default: 76 panic("Internal error") 77 } 78 } 79 80 func (kind MsgKind) Icon() string { 81 // Special-case Windows command prompt, which only supports a few characters 82 if isProbablyWindowsCommandPrompt() { 83 switch kind { 84 case Error: 85 return "X" 86 case Warning: 87 return "▲" 88 case Info: 89 return "►" 90 case Note: 91 return "→" 92 case Debug: 93 return "●" 94 case Verbose: 95 return "♦" 96 default: 97 panic("Internal error") 98 } 99 } 100 101 switch kind { 102 case Error: 103 return "✘" 104 case Warning: 105 return "▲" 106 case Info: 107 return "▶" 108 case Note: 109 return "→" 110 case Debug: 111 return "●" 112 case Verbose: 113 return "⬥" 114 default: 115 panic("Internal error") 116 } 117 } 118 119 var windowsCommandPrompt struct { 120 mutex sync.Mutex 121 once bool 122 isProbablyCMD bool 123 } 124 125 func isProbablyWindowsCommandPrompt() bool { 126 windowsCommandPrompt.mutex.Lock() 127 defer windowsCommandPrompt.mutex.Unlock() 128 129 if !windowsCommandPrompt.once { 130 windowsCommandPrompt.once = true 131 132 // Assume we are running in Windows Command Prompt if we're on Windows. If 133 // so, we can't use emoji because it won't be supported. Except we can 134 // still use emoji if the WT_SESSION environment variable is present 135 // because that means we're running in the new Windows Terminal instead. 136 if runtime.GOOS == "windows" { 137 windowsCommandPrompt.isProbablyCMD = true 138 if _, ok := os.LookupEnv("WT_SESSION"); ok { 139 windowsCommandPrompt.isProbablyCMD = false 140 } 141 } 142 } 143 144 return windowsCommandPrompt.isProbablyCMD 145 } 146 147 type Msg struct { 148 Notes []MsgData 149 PluginName string 150 Data MsgData 151 Kind MsgKind 152 ID MsgID 153 } 154 155 type MsgData struct { 156 // Optional user-specified data that is passed through unmodified 157 UserDetail interface{} 158 159 Location *MsgLocation 160 Text string 161 162 DisableMaximumWidth bool 163 } 164 165 type MsgLocation struct { 166 File string 167 Namespace string 168 LineText string 169 Suggestion string 170 Line int // 1-based 171 Column int // 0-based, in bytes 172 Length int // in bytes 173 } 174 175 type Loc struct { 176 // This is the 0-based index of this location from the start of the file, in bytes 177 Start int32 178 } 179 180 type Range struct { 181 Loc Loc 182 Len int32 183 } 184 185 func (r Range) End() int32 { 186 return r.Loc.Start + r.Len 187 } 188 189 func (a *Range) ExpandBy(b Range) { 190 if a.Len == 0 { 191 *a = b 192 } else { 193 end := a.End() 194 if n := b.End(); n > end { 195 end = n 196 } 197 if b.Loc.Start < a.Loc.Start { 198 a.Loc.Start = b.Loc.Start 199 } 200 a.Len = end - a.Loc.Start 201 } 202 } 203 204 type Span struct { 205 Text string 206 Range Range 207 } 208 209 // This type is just so we can use Go's native sort function 210 type SortableMsgs []Msg 211 212 func (a SortableMsgs) Len() int { return len(a) } 213 func (a SortableMsgs) Swap(i int, j int) { a[i], a[j] = a[j], a[i] } 214 215 func (a SortableMsgs) Less(i int, j int) bool { 216 ai := a[i] 217 aj := a[j] 218 aiLoc := ai.Data.Location 219 ajLoc := aj.Data.Location 220 if aiLoc == nil || ajLoc == nil { 221 return aiLoc == nil && ajLoc != nil 222 } 223 if aiLoc.File != ajLoc.File { 224 return aiLoc.File < ajLoc.File 225 } 226 if aiLoc.Line != ajLoc.Line { 227 return aiLoc.Line < ajLoc.Line 228 } 229 if aiLoc.Column != ajLoc.Column { 230 return aiLoc.Column < ajLoc.Column 231 } 232 if ai.Kind != aj.Kind { 233 return ai.Kind < aj.Kind 234 } 235 return ai.Data.Text < aj.Data.Text 236 } 237 238 // This is used to represent both file system paths (Namespace == "file") and 239 // abstract module paths (Namespace != "file"). Abstract module paths represent 240 // "virtual modules" when used for an input file and "package paths" when used 241 // to represent an external module. 242 type Path struct { 243 Text string 244 Namespace string 245 246 // This feature was added to support ancient CSS libraries that append things 247 // like "?#iefix" and "#icons" to some of their import paths as a hack for IE6. 248 // The intent is for these suffix parts to be ignored but passed through to 249 // the output. This is supported by other bundlers, so we also support this. 250 IgnoredSuffix string 251 252 // Import attributes (the "with" keyword after an import) can affect path 253 // resolution. In other words, two paths in the same file that are otherwise 254 // equal but that have different import attributes may resolve to different 255 // paths. 256 ImportAttributes ImportAttributes 257 258 Flags PathFlags 259 } 260 261 // We rely on paths as map keys. Go doesn't support custom hash codes and 262 // only implements hash codes for certain types. In particular, hash codes 263 // are implemented for strings but not for arrays of strings. So we have to 264 // pack these import attributes into a string. 265 type ImportAttributes struct { 266 packedData string 267 } 268 269 type ImportAttribute struct { 270 Key string 271 Value string 272 } 273 274 // This returns a sorted array instead of a map to make determinism easier 275 func (attrs ImportAttributes) DecodeIntoArray() (result []ImportAttribute) { 276 if attrs.packedData == "" { 277 return nil 278 } 279 bytes := []byte(attrs.packedData) 280 for len(bytes) > 0 { 281 kn := 4 + binary.LittleEndian.Uint32(bytes[:4]) 282 k := string(bytes[4:kn]) 283 bytes = bytes[kn:] 284 vn := 4 + binary.LittleEndian.Uint32(bytes[:4]) 285 v := string(bytes[4:vn]) 286 bytes = bytes[vn:] 287 result = append(result, ImportAttribute{Key: k, Value: v}) 288 } 289 return result 290 } 291 292 func (attrs ImportAttributes) DecodeIntoMap() (result map[string]string) { 293 if array := attrs.DecodeIntoArray(); len(array) > 0 { 294 result = make(map[string]string, len(array)) 295 for _, attr := range array { 296 result[attr.Key] = attr.Value 297 } 298 } 299 return 300 } 301 302 func EncodeImportAttributes(value map[string]string) ImportAttributes { 303 if len(value) == 0 { 304 return ImportAttributes{} 305 } 306 keys := make([]string, 0, len(value)) 307 for k := range value { 308 keys = append(keys, k) 309 } 310 sort.Strings(keys) 311 var sb strings.Builder 312 var n [4]byte 313 for _, k := range keys { 314 v := value[k] 315 binary.LittleEndian.PutUint32(n[:], uint32(len(k))) 316 sb.Write(n[:]) 317 sb.WriteString(k) 318 binary.LittleEndian.PutUint32(n[:], uint32(len(v))) 319 sb.Write(n[:]) 320 sb.WriteString(v) 321 } 322 return ImportAttributes{packedData: sb.String()} 323 } 324 325 type PathFlags uint8 326 327 const ( 328 // This corresponds to a value of "false' in the "browser" package.json field 329 PathDisabled PathFlags = 1 << iota 330 ) 331 332 func (p Path) IsDisabled() bool { 333 return (p.Flags & PathDisabled) != 0 334 } 335 336 var noColorResult bool 337 var noColorOnce sync.Once 338 339 func hasNoColorEnvironmentVariable() bool { 340 noColorOnce.Do(func() { 341 // Read "NO_COLOR" from the environment. This is a convention that some 342 // software follows. See https://no-color.org/ for more information. 343 if _, ok := os.LookupEnv("NO_COLOR"); ok { 344 noColorResult = true 345 } 346 }) 347 return noColorResult 348 } 349 350 // This has a custom implementation instead of using "filepath.Dir/Base/Ext" 351 // because it should work the same on Unix and Windows. These names end up in 352 // the generated output and the generated output should not depend on the OS. 353 func PlatformIndependentPathDirBaseExt(path string) (dir string, base string, ext string) { 354 absRootSlash := -1 355 356 // Make sure we don't strip off the slash for the root of the file system 357 if len(path) > 0 && (path[0] == '/' || path[0] == '\\') { 358 absRootSlash = 0 // Unix 359 } else if len(path) > 2 && path[1] == ':' && (path[2] == '/' || path[2] == '\\') { 360 if c := path[0]; (c >= 'a' && c < 'z') || (c >= 'A' && c <= 'Z') { 361 absRootSlash = 2 // Windows 362 } 363 } 364 365 for { 366 i := strings.LastIndexAny(path, "/\\") 367 368 // Stop if there are no more slashes 369 if i < 0 { 370 base = path 371 break 372 } 373 374 // Stop if we found a non-trailing slash 375 if i == absRootSlash { 376 dir, base = path[:i+1], path[i+1:] 377 break 378 } 379 if i+1 != len(path) { 380 dir, base = path[:i], path[i+1:] 381 break 382 } 383 384 // Ignore trailing slashes 385 path = path[:i] 386 } 387 388 // Strip off the extension 389 if dot := strings.LastIndexByte(base, '.'); dot >= 0 { 390 ext = base[dot:] 391 392 // We default to the "local-css" loader for ".module.css" files. Make sure 393 // the string names generated by this don't all have "_module_" in them. 394 if ext == ".css" { 395 if dot2 := strings.LastIndexByte(base[:dot], '.'); dot2 >= 0 && base[dot2:] == ".module.css" { 396 dot = dot2 397 ext = base[dot:] 398 } 399 } 400 401 base = base[:dot] 402 } 403 return 404 } 405 406 type Source struct { 407 // This is used for error messages and the metadata JSON file. 408 // 409 // This is a mostly platform-independent path. It's relative to the current 410 // working directory and always uses standard path separators. Use this for 411 // referencing a file in all output data. These paths still use the original 412 // case of the path so they may still work differently on file systems that 413 // are case-insensitive vs. case-sensitive. 414 PrettyPath string 415 416 // An identifier that is mixed in to automatically-generated symbol names to 417 // improve readability. For example, if the identifier is "util" then the 418 // symbol for an "export default" statement will be called "util_default". 419 IdentifierName string 420 421 Contents string 422 423 // This is used as a unique key to identify this source file. It should never 424 // be shown to the user (e.g. never print this to the terminal). 425 // 426 // If it's marked as an absolute path, it's a platform-dependent path that 427 // includes environment-specific things such as Windows backslash path 428 // separators and potentially the user's home directory. Only use this for 429 // passing to syscalls for reading and writing to the file system. Do not 430 // include this in any output data. 431 // 432 // If it's marked as not an absolute path, it's an opaque string that is used 433 // to refer to an automatically-generated module. 434 KeyPath Path 435 436 Index uint32 437 } 438 439 func (s *Source) TextForRange(r Range) string { 440 return s.Contents[r.Loc.Start : r.Loc.Start+r.Len] 441 } 442 443 func (s *Source) LocBeforeWhitespace(loc Loc) Loc { 444 for loc.Start > 0 { 445 c, width := utf8.DecodeLastRuneInString(s.Contents[:loc.Start]) 446 if c != ' ' && c != '\t' && c != '\r' && c != '\n' { 447 break 448 } 449 loc.Start -= int32(width) 450 } 451 return loc 452 } 453 454 func (s *Source) RangeOfOperatorBefore(loc Loc, op string) Range { 455 text := s.Contents[:loc.Start] 456 index := strings.LastIndex(text, op) 457 if index >= 0 { 458 return Range{Loc: Loc{Start: int32(index)}, Len: int32(len(op))} 459 } 460 return Range{Loc: loc} 461 } 462 463 func (s *Source) RangeOfOperatorAfter(loc Loc, op string) Range { 464 text := s.Contents[loc.Start:] 465 index := strings.Index(text, op) 466 if index >= 0 { 467 return Range{Loc: Loc{Start: loc.Start + int32(index)}, Len: int32(len(op))} 468 } 469 return Range{Loc: loc} 470 } 471 472 func (s *Source) RangeOfString(loc Loc) Range { 473 text := s.Contents[loc.Start:] 474 if len(text) == 0 { 475 return Range{Loc: loc, Len: 0} 476 } 477 478 quote := text[0] 479 if quote == '"' || quote == '\'' { 480 // Search for the matching quote character 481 for i := 1; i < len(text); i++ { 482 c := text[i] 483 if c == quote { 484 return Range{Loc: loc, Len: int32(i + 1)} 485 } else if c == '\\' { 486 i += 1 487 } 488 } 489 } 490 491 if quote == '`' { 492 // Search for the matching quote character 493 for i := 1; i < len(text); i++ { 494 c := text[i] 495 if c == quote { 496 return Range{Loc: loc, Len: int32(i + 1)} 497 } else if c == '\\' { 498 i += 1 499 } else if c == '$' && i+1 < len(text) && text[i+1] == '{' { 500 break // Only return the range for no-substitution template literals 501 } 502 } 503 } 504 505 return Range{Loc: loc, Len: 0} 506 } 507 508 func (s *Source) RangeOfNumber(loc Loc) (r Range) { 509 text := s.Contents[loc.Start:] 510 r = Range{Loc: loc, Len: 0} 511 512 if len(text) > 0 { 513 if c := text[0]; c >= '0' && c <= '9' { 514 r.Len = 1 515 for int(r.Len) < len(text) { 516 c := text[r.Len] 517 if (c < '0' || c > '9') && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '.' && c != '_' { 518 break 519 } 520 r.Len++ 521 } 522 } 523 } 524 return 525 } 526 527 func (s *Source) RangeOfLegacyOctalEscape(loc Loc) (r Range) { 528 text := s.Contents[loc.Start:] 529 r = Range{Loc: loc, Len: 0} 530 531 if len(text) >= 2 && text[0] == '\\' { 532 r.Len = 2 533 for r.Len < 4 && int(r.Len) < len(text) { 534 c := text[r.Len] 535 if c < '0' || c > '9' { 536 break 537 } 538 r.Len++ 539 } 540 } 541 return 542 } 543 544 func (s *Source) CommentTextWithoutIndent(r Range) string { 545 text := s.Contents[r.Loc.Start:r.End()] 546 if len(text) < 2 || !strings.HasPrefix(text, "/*") { 547 return text 548 } 549 prefix := s.Contents[:r.Loc.Start] 550 551 // Figure out the initial indent 552 indent := 0 553 seekBackwardToNewline: 554 for len(prefix) > 0 { 555 c, size := utf8.DecodeLastRuneInString(prefix) 556 switch c { 557 case '\r', '\n', '\u2028', '\u2029': 558 break seekBackwardToNewline 559 } 560 prefix = prefix[:len(prefix)-size] 561 indent++ 562 } 563 564 // Split the comment into lines 565 var lines []string 566 start := 0 567 for i, c := range text { 568 switch c { 569 case '\r', '\n': 570 // Don't double-append for Windows style "\r\n" newlines 571 if start <= i { 572 lines = append(lines, text[start:i]) 573 } 574 575 start = i + 1 576 577 // Ignore the second part of Windows style "\r\n" newlines 578 if c == '\r' && start < len(text) && text[start] == '\n' { 579 start++ 580 } 581 582 case '\u2028', '\u2029': 583 lines = append(lines, text[start:i]) 584 start = i + 3 585 } 586 } 587 lines = append(lines, text[start:]) 588 589 // Find the minimum indent over all lines after the first line 590 for _, line := range lines[1:] { 591 lineIndent := 0 592 for _, c := range line { 593 if c != ' ' && c != '\t' { 594 break 595 } 596 lineIndent++ 597 } 598 if indent > lineIndent { 599 indent = lineIndent 600 } 601 } 602 603 // Trim the indent off of all lines after the first line 604 for i, line := range lines { 605 if i > 0 { 606 lines[i] = line[indent:] 607 } 608 } 609 return strings.Join(lines, "\n") 610 } 611 612 func plural(prefix string, count int, shown int, someAreMissing bool) string { 613 var text string 614 if count == 1 { 615 text = fmt.Sprintf("%d %s", count, prefix) 616 } else { 617 text = fmt.Sprintf("%d %ss", count, prefix) 618 } 619 if shown < count { 620 text = fmt.Sprintf("%d of %s", shown, text) 621 } else if someAreMissing && count > 1 { 622 text = "all " + text 623 } 624 return text 625 } 626 627 func errorAndWarningSummary(errors int, warnings int, shownErrors int, shownWarnings int) string { 628 someAreMissing := shownWarnings < warnings || shownErrors < errors 629 switch { 630 case errors == 0: 631 return plural("warning", warnings, shownWarnings, someAreMissing) 632 case warnings == 0: 633 return plural("error", errors, shownErrors, someAreMissing) 634 default: 635 return fmt.Sprintf("%s and %s", 636 plural("warning", warnings, shownWarnings, someAreMissing), 637 plural("error", errors, shownErrors, someAreMissing)) 638 } 639 } 640 641 type APIKind uint8 642 643 const ( 644 GoAPI APIKind = iota 645 CLIAPI 646 JSAPI 647 ) 648 649 // This can be used to customize error messages for the current API kind 650 var API APIKind 651 652 type TerminalInfo struct { 653 IsTTY bool 654 UseColorEscapes bool 655 Width int 656 Height int 657 } 658 659 func NewStderrLog(options OutputOptions) Log { 660 var mutex sync.Mutex 661 var msgs SortableMsgs 662 terminalInfo := GetTerminalInfo(os.Stderr) 663 errors := 0 664 warnings := 0 665 shownErrors := 0 666 shownWarnings := 0 667 hasErrors := false 668 remainingMessagesBeforeLimit := options.MessageLimit 669 if remainingMessagesBeforeLimit == 0 { 670 remainingMessagesBeforeLimit = 0x7FFFFFFF 671 } 672 var deferredWarnings []Msg 673 674 finalizeLog := func() { 675 // Print the deferred warning now if there was no error after all 676 for remainingMessagesBeforeLimit > 0 && len(deferredWarnings) > 0 { 677 shownWarnings++ 678 writeStringWithColor(os.Stderr, deferredWarnings[0].String(options, terminalInfo)) 679 deferredWarnings = deferredWarnings[1:] 680 remainingMessagesBeforeLimit-- 681 } 682 683 // Print out a summary 684 if options.MessageLimit > 0 && errors+warnings > options.MessageLimit { 685 writeStringWithColor(os.Stderr, fmt.Sprintf("%s shown (disable the message limit with --log-limit=0)\n", 686 errorAndWarningSummary(errors, warnings, shownErrors, shownWarnings))) 687 } else if options.LogLevel <= LevelInfo && (warnings != 0 || errors != 0) { 688 writeStringWithColor(os.Stderr, fmt.Sprintf("%s\n", 689 errorAndWarningSummary(errors, warnings, shownErrors, shownWarnings))) 690 } 691 } 692 693 switch options.Color { 694 case ColorNever: 695 terminalInfo.UseColorEscapes = false 696 case ColorAlways: 697 terminalInfo.UseColorEscapes = SupportsColorEscapes 698 } 699 700 return Log{ 701 Level: options.LogLevel, 702 Overrides: options.Overrides, 703 704 AddMsg: func(msg Msg) { 705 mutex.Lock() 706 defer mutex.Unlock() 707 msgs = append(msgs, msg) 708 709 switch msg.Kind { 710 case Verbose: 711 if options.LogLevel <= LevelVerbose { 712 writeStringWithColor(os.Stderr, msg.String(options, terminalInfo)) 713 } 714 715 case Debug: 716 if options.LogLevel <= LevelDebug { 717 writeStringWithColor(os.Stderr, msg.String(options, terminalInfo)) 718 } 719 720 case Info: 721 if options.LogLevel <= LevelInfo { 722 writeStringWithColor(os.Stderr, msg.String(options, terminalInfo)) 723 } 724 725 case Error: 726 hasErrors = true 727 if options.LogLevel <= LevelError { 728 errors++ 729 } 730 731 case Warning: 732 if options.LogLevel <= LevelWarning { 733 warnings++ 734 } 735 } 736 737 // Be silent if we're past the limit so we don't flood the terminal 738 if remainingMessagesBeforeLimit == 0 { 739 return 740 } 741 742 switch msg.Kind { 743 case Error: 744 if options.LogLevel <= LevelError { 745 shownErrors++ 746 writeStringWithColor(os.Stderr, msg.String(options, terminalInfo)) 747 remainingMessagesBeforeLimit-- 748 } 749 750 case Warning: 751 if options.LogLevel <= LevelWarning { 752 if remainingMessagesBeforeLimit > (options.MessageLimit+1)/2 { 753 shownWarnings++ 754 writeStringWithColor(os.Stderr, msg.String(options, terminalInfo)) 755 remainingMessagesBeforeLimit-- 756 } else { 757 // If we have less than half of the slots left, wait for potential 758 // future errors instead of using up all of the slots with warnings. 759 // We want the log for a failed build to always have at least one 760 // error in it. 761 deferredWarnings = append(deferredWarnings, msg) 762 } 763 } 764 } 765 }, 766 767 HasErrors: func() bool { 768 mutex.Lock() 769 defer mutex.Unlock() 770 return hasErrors 771 }, 772 773 Peek: func() []Msg { 774 mutex.Lock() 775 defer mutex.Unlock() 776 sort.Stable(msgs) 777 return append([]Msg{}, msgs...) 778 }, 779 780 Done: func() []Msg { 781 mutex.Lock() 782 defer mutex.Unlock() 783 finalizeLog() 784 sort.Stable(msgs) 785 return msgs 786 }, 787 } 788 } 789 790 func PrintErrorToStderr(osArgs []string, text string) { 791 PrintMessageToStderr(osArgs, Msg{Kind: Error, Data: MsgData{Text: text}}) 792 } 793 794 func PrintErrorWithNoteToStderr(osArgs []string, text string, note string) { 795 msg := Msg{ 796 Kind: Error, 797 Data: MsgData{Text: text}, 798 } 799 if note != "" { 800 msg.Notes = []MsgData{{Text: note}} 801 } 802 PrintMessageToStderr(osArgs, msg) 803 } 804 805 func OutputOptionsForArgs(osArgs []string) OutputOptions { 806 options := OutputOptions{IncludeSource: true} 807 808 // Implement a mini argument parser so these options always work even if we 809 // haven't yet gotten to the general-purpose argument parsing code 810 for _, arg := range osArgs { 811 switch arg { 812 case "--color=false": 813 options.Color = ColorNever 814 case "--color=true", "--color": 815 options.Color = ColorAlways 816 case "--log-level=info": 817 options.LogLevel = LevelInfo 818 case "--log-level=warning": 819 options.LogLevel = LevelWarning 820 case "--log-level=error": 821 options.LogLevel = LevelError 822 case "--log-level=silent": 823 options.LogLevel = LevelSilent 824 } 825 } 826 827 return options 828 } 829 830 func PrintMessageToStderr(osArgs []string, msg Msg) { 831 log := NewStderrLog(OutputOptionsForArgs(osArgs)) 832 log.AddMsg(msg) 833 log.Done() 834 } 835 836 type Colors struct { 837 Reset string 838 Bold string 839 Dim string 840 Underline string 841 842 Red string 843 Green string 844 Blue string 845 846 Cyan string 847 Magenta string 848 Yellow string 849 850 RedBgRed string 851 RedBgWhite string 852 GreenBgGreen string 853 GreenBgWhite string 854 BlueBgBlue string 855 BlueBgWhite string 856 857 CyanBgCyan string 858 CyanBgBlack string 859 MagentaBgMagenta string 860 MagentaBgBlack string 861 YellowBgYellow string 862 YellowBgBlack string 863 } 864 865 var TerminalColors = Colors{ 866 Reset: "\033[0m", 867 Bold: "\033[1m", 868 Dim: "\033[37m", 869 Underline: "\033[4m", 870 871 Red: "\033[31m", 872 Green: "\033[32m", 873 Blue: "\033[34m", 874 875 Cyan: "\033[36m", 876 Magenta: "\033[35m", 877 Yellow: "\033[33m", 878 879 RedBgRed: "\033[41;31m", 880 RedBgWhite: "\033[41;97m", 881 GreenBgGreen: "\033[42;32m", 882 GreenBgWhite: "\033[42;97m", 883 BlueBgBlue: "\033[44;34m", 884 BlueBgWhite: "\033[44;97m", 885 886 CyanBgCyan: "\033[46;36m", 887 CyanBgBlack: "\033[46;30m", 888 MagentaBgMagenta: "\033[45;35m", 889 MagentaBgBlack: "\033[45;30m", 890 YellowBgYellow: "\033[43;33m", 891 YellowBgBlack: "\033[43;30m", 892 } 893 894 func PrintText(file *os.File, level LogLevel, osArgs []string, callback func(Colors) string) { 895 options := OutputOptionsForArgs(osArgs) 896 897 // Skip logging these if these logs are disabled 898 if options.LogLevel > level { 899 return 900 } 901 902 PrintTextWithColor(file, options.Color, callback) 903 } 904 905 func PrintTextWithColor(file *os.File, useColor UseColor, callback func(Colors) string) { 906 var useColorEscapes bool 907 switch useColor { 908 case ColorNever: 909 useColorEscapes = false 910 case ColorAlways: 911 useColorEscapes = SupportsColorEscapes 912 case ColorIfTerminal: 913 useColorEscapes = GetTerminalInfo(file).UseColorEscapes 914 } 915 916 var colors Colors 917 if useColorEscapes { 918 colors = TerminalColors 919 } 920 writeStringWithColor(file, callback(colors)) 921 } 922 923 type SummaryTableEntry struct { 924 Dir string 925 Base string 926 Size string 927 Bytes int 928 IsSourceMap bool 929 } 930 931 // This type is just so we can use Go's native sort function 932 type SummaryTable []SummaryTableEntry 933 934 func (t SummaryTable) Len() int { return len(t) } 935 func (t SummaryTable) Swap(i int, j int) { t[i], t[j] = t[j], t[i] } 936 937 func (t SummaryTable) Less(i int, j int) bool { 938 ti := t[i] 939 tj := t[j] 940 941 // Sort source maps last 942 if !ti.IsSourceMap && tj.IsSourceMap { 943 return true 944 } 945 if ti.IsSourceMap && !tj.IsSourceMap { 946 return false 947 } 948 949 // Sort by size first 950 if ti.Bytes > tj.Bytes { 951 return true 952 } 953 if ti.Bytes < tj.Bytes { 954 return false 955 } 956 957 // Sort alphabetically by directory first 958 if ti.Dir < tj.Dir { 959 return true 960 } 961 if ti.Dir > tj.Dir { 962 return false 963 } 964 965 // Then sort alphabetically by file name 966 return ti.Base < tj.Base 967 } 968 969 // Show a warning icon next to output files that are 1mb or larger 970 const sizeWarningThreshold = 1024 * 1024 971 972 func PrintSummary(useColor UseColor, table SummaryTable, start *time.Time) { 973 PrintTextWithColor(os.Stderr, useColor, func(colors Colors) string { 974 isProbablyWindowsCommandPrompt := isProbablyWindowsCommandPrompt() 975 sb := strings.Builder{} 976 977 if len(table) > 0 { 978 info := GetTerminalInfo(os.Stderr) 979 980 // Truncate the table in case it's really long 981 maxLength := info.Height / 2 982 if info.Height == 0 { 983 maxLength = 20 984 } else if maxLength < 5 { 985 maxLength = 5 986 } 987 length := len(table) 988 sort.Sort(table) 989 if length > maxLength { 990 table = table[:maxLength] 991 } 992 993 // Compute the maximum width of the size column 994 spacingBetweenColumns := 2 995 hasSizeWarning := false 996 maxPath := 0 997 maxSize := 0 998 for _, entry := range table { 999 path := len(entry.Dir) + len(entry.Base) 1000 size := len(entry.Size) + spacingBetweenColumns 1001 if path > maxPath { 1002 maxPath = path 1003 } 1004 if size > maxSize { 1005 maxSize = size 1006 } 1007 if !entry.IsSourceMap && entry.Bytes >= sizeWarningThreshold { 1008 hasSizeWarning = true 1009 } 1010 } 1011 1012 margin := " " 1013 layoutWidth := info.Width 1014 if layoutWidth < 1 { 1015 layoutWidth = defaultTerminalWidth 1016 } 1017 layoutWidth -= 2 * len(margin) 1018 if hasSizeWarning { 1019 // Add space for the warning icon 1020 layoutWidth -= 2 1021 } 1022 if layoutWidth > maxPath+maxSize { 1023 layoutWidth = maxPath + maxSize 1024 } 1025 sb.WriteByte('\n') 1026 1027 for _, entry := range table { 1028 dir, base := entry.Dir, entry.Base 1029 pathWidth := layoutWidth - maxSize 1030 1031 // Truncate the path with "..." to fit on one line 1032 if len(dir)+len(base) > pathWidth { 1033 // Trim the directory from the front, leaving the trailing slash 1034 if len(dir) > 0 { 1035 n := pathWidth - len(base) - 3 1036 if n < 1 { 1037 n = 1 1038 } 1039 dir = "..." + dir[len(dir)-n:] 1040 } 1041 1042 // Trim the file name from the back 1043 if len(dir)+len(base) > pathWidth { 1044 n := pathWidth - len(dir) - 3 1045 if n < 0 { 1046 n = 0 1047 } 1048 base = base[:n] + "..." 1049 } 1050 } 1051 1052 spacer := layoutWidth - len(entry.Size) - len(dir) - len(base) 1053 if spacer < 0 { 1054 spacer = 0 1055 } 1056 1057 // Put a warning next to the size if it's above a certain threshold 1058 sizeColor := colors.Cyan 1059 sizeWarning := "" 1060 if !entry.IsSourceMap && entry.Bytes >= sizeWarningThreshold { 1061 sizeColor = colors.Yellow 1062 1063 // Emoji don't work in Windows Command Prompt 1064 if !isProbablyWindowsCommandPrompt { 1065 sizeWarning = " ⚠️" 1066 } 1067 } 1068 1069 sb.WriteString(fmt.Sprintf("%s%s%s%s%s%s%s%s%s%s%s%s\n", 1070 margin, 1071 colors.Dim, 1072 dir, 1073 colors.Reset, 1074 colors.Bold, 1075 base, 1076 colors.Reset, 1077 strings.Repeat(" ", spacer), 1078 sizeColor, 1079 entry.Size, 1080 sizeWarning, 1081 colors.Reset, 1082 )) 1083 } 1084 1085 // Say how many remaining files are not shown 1086 if length > maxLength { 1087 plural := "s" 1088 if length == maxLength+1 { 1089 plural = "" 1090 } 1091 sb.WriteString(fmt.Sprintf("%s%s...and %d more output file%s...%s\n", margin, colors.Dim, length-maxLength, plural, colors.Reset)) 1092 } 1093 } 1094 sb.WriteByte('\n') 1095 1096 lightningSymbol := "⚡ " 1097 1098 // Emoji don't work in Windows Command Prompt 1099 if isProbablyWindowsCommandPrompt { 1100 lightningSymbol = "" 1101 } 1102 1103 // Printing the time taken is optional 1104 if start != nil { 1105 sb.WriteString(fmt.Sprintf("%s%sDone in %dms%s\n", 1106 lightningSymbol, 1107 colors.Green, 1108 time.Since(*start).Milliseconds(), 1109 colors.Reset, 1110 )) 1111 } 1112 1113 return sb.String() 1114 }) 1115 } 1116 1117 type DeferLogKind uint8 1118 1119 const ( 1120 DeferLogAll DeferLogKind = iota 1121 DeferLogNoVerboseOrDebug 1122 ) 1123 1124 func NewDeferLog(kind DeferLogKind, overrides map[MsgID]LogLevel) Log { 1125 var msgs SortableMsgs 1126 var mutex sync.Mutex 1127 var hasErrors bool 1128 1129 return Log{ 1130 Level: LevelInfo, 1131 Overrides: overrides, 1132 1133 AddMsg: func(msg Msg) { 1134 if kind == DeferLogNoVerboseOrDebug && (msg.Kind == Verbose || msg.Kind == Debug) { 1135 return 1136 } 1137 mutex.Lock() 1138 defer mutex.Unlock() 1139 if msg.Kind == Error { 1140 hasErrors = true 1141 } 1142 msgs = append(msgs, msg) 1143 }, 1144 1145 HasErrors: func() bool { 1146 mutex.Lock() 1147 defer mutex.Unlock() 1148 return hasErrors 1149 }, 1150 1151 Peek: func() []Msg { 1152 mutex.Lock() 1153 defer mutex.Unlock() 1154 return append([]Msg{}, msgs...) 1155 }, 1156 1157 Done: func() []Msg { 1158 mutex.Lock() 1159 defer mutex.Unlock() 1160 sort.Stable(msgs) 1161 return msgs 1162 }, 1163 } 1164 } 1165 1166 type UseColor uint8 1167 1168 const ( 1169 ColorIfTerminal UseColor = iota 1170 ColorNever 1171 ColorAlways 1172 ) 1173 1174 type OutputOptions struct { 1175 MessageLimit int 1176 IncludeSource bool 1177 Color UseColor 1178 LogLevel LogLevel 1179 Overrides map[MsgID]LogLevel 1180 } 1181 1182 func (msg Msg) String(options OutputOptions, terminalInfo TerminalInfo) string { 1183 // Format the message 1184 text := msgString(options.IncludeSource, terminalInfo, msg.ID, msg.Kind, msg.Data, msg.PluginName) 1185 1186 // Format the notes 1187 var oldData MsgData 1188 for i, note := range msg.Notes { 1189 if options.IncludeSource && (i == 0 || strings.IndexByte(oldData.Text, '\n') >= 0 || oldData.Location != nil) { 1190 text += "\n" 1191 } 1192 text += msgString(options.IncludeSource, terminalInfo, MsgID_None, Note, note, "") 1193 oldData = note 1194 } 1195 1196 // Add extra spacing between messages if source code is present 1197 if options.IncludeSource { 1198 text += "\n" 1199 } 1200 return text 1201 } 1202 1203 // The number of margin characters in addition to the line number 1204 const extraMarginChars = 9 1205 1206 func marginWithLineText(maxMargin int, line int) string { 1207 number := fmt.Sprintf("%d", line) 1208 return fmt.Sprintf(" %s%s │ ", strings.Repeat(" ", maxMargin-len(number)), number) 1209 } 1210 1211 func emptyMarginText(maxMargin int, isLast bool) string { 1212 space := strings.Repeat(" ", maxMargin) 1213 if isLast { 1214 return fmt.Sprintf(" %s ╵ ", space) 1215 } 1216 return fmt.Sprintf(" %s │ ", space) 1217 } 1218 1219 func msgString(includeSource bool, terminalInfo TerminalInfo, id MsgID, kind MsgKind, data MsgData, pluginName string) string { 1220 if !includeSource { 1221 if loc := data.Location; loc != nil { 1222 return fmt.Sprintf("%s: %s: %s\n", loc.File, kind.String(), data.Text) 1223 } 1224 return fmt.Sprintf("%s: %s\n", kind.String(), data.Text) 1225 } 1226 1227 var colors Colors 1228 if terminalInfo.UseColorEscapes { 1229 colors = TerminalColors 1230 } 1231 1232 var iconColor string 1233 var kindColorBrackets string 1234 var kindColorText string 1235 1236 location := "" 1237 1238 if data.Location != nil { 1239 maxMargin := len(fmt.Sprintf("%d", data.Location.Line)) 1240 d := detailStruct(data, terminalInfo, maxMargin) 1241 1242 if d.Suggestion != "" { 1243 location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s%s%s%s%s\n%s", 1244 d.Path, d.Line, d.Column, 1245 colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter, 1246 emptyMarginText(maxMargin, false), d.Indent, colors.Green, d.Marker, colors.Dim, 1247 emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Suggestion, colors.Reset, 1248 d.ContentAfter, 1249 ) 1250 } else { 1251 location = fmt.Sprintf("\n %s:%d:%d:\n%s%s%s%s%s%s\n%s%s%s%s%s\n%s", 1252 d.Path, d.Line, d.Column, 1253 colors.Dim, d.SourceBefore, colors.Green, d.SourceMarked, colors.Dim, d.SourceAfter, 1254 emptyMarginText(maxMargin, true), d.Indent, colors.Green, d.Marker, colors.Reset, 1255 d.ContentAfter, 1256 ) 1257 } 1258 } 1259 1260 switch kind { 1261 case Verbose: 1262 iconColor = colors.Cyan 1263 kindColorBrackets = colors.CyanBgCyan 1264 kindColorText = colors.CyanBgBlack 1265 1266 case Debug: 1267 iconColor = colors.Green 1268 kindColorBrackets = colors.GreenBgGreen 1269 kindColorText = colors.GreenBgWhite 1270 1271 case Info: 1272 iconColor = colors.Blue 1273 kindColorBrackets = colors.BlueBgBlue 1274 kindColorText = colors.BlueBgWhite 1275 1276 case Error: 1277 iconColor = colors.Red 1278 kindColorBrackets = colors.RedBgRed 1279 kindColorText = colors.RedBgWhite 1280 1281 case Warning: 1282 iconColor = colors.Yellow 1283 kindColorBrackets = colors.YellowBgYellow 1284 kindColorText = colors.YellowBgBlack 1285 1286 case Note: 1287 sb := strings.Builder{} 1288 1289 for _, line := range strings.Split(data.Text, "\n") { 1290 // Special-case word wrapping 1291 if wrapWidth := terminalInfo.Width; wrapWidth > 2 { 1292 if !data.DisableMaximumWidth && wrapWidth > 100 { 1293 wrapWidth = 100 // Enforce a maximum paragraph width for readability 1294 } 1295 for _, run := range wrapWordsInString(line, wrapWidth-2) { 1296 sb.WriteString(" ") 1297 sb.WriteString(linkifyText(run, colors.Underline, colors.Reset)) 1298 sb.WriteByte('\n') 1299 } 1300 continue 1301 } 1302 1303 // Otherwise, just write an indented line 1304 sb.WriteString(" ") 1305 sb.WriteString(linkifyText(line, colors.Underline, colors.Reset)) 1306 sb.WriteByte('\n') 1307 } 1308 1309 sb.WriteString(location) 1310 return sb.String() 1311 } 1312 1313 if pluginName != "" { 1314 pluginName = fmt.Sprintf(" %s%s[plugin %s]%s", colors.Bold, colors.Magenta, pluginName, colors.Reset) 1315 } 1316 1317 msgID := MsgIDToString(id) 1318 if msgID != "" { 1319 msgID = fmt.Sprintf(" [%s]", msgID) 1320 } 1321 1322 return fmt.Sprintf("%s%s %s[%s%s%s]%s %s%s%s%s%s\n%s", 1323 iconColor, kind.Icon(), 1324 kindColorBrackets, kindColorText, kind.String(), kindColorBrackets, colors.Reset, 1325 colors.Bold, data.Text, colors.Reset, pluginName, msgID, 1326 location, 1327 ) 1328 } 1329 1330 func linkifyText(text string, underline string, reset string) string { 1331 if underline == "" { 1332 return text 1333 } 1334 1335 https := strings.Index(text, "https://") 1336 if https == -1 { 1337 return text 1338 } 1339 1340 sb := strings.Builder{} 1341 for { 1342 https := strings.Index(text, "https://") 1343 if https == -1 { 1344 break 1345 } 1346 1347 end := strings.IndexByte(text[https:], ' ') 1348 if end == -1 { 1349 end = len(text) 1350 } else { 1351 end += https 1352 } 1353 1354 // Remove trailing punctuation 1355 if end > https { 1356 switch text[end-1] { 1357 case '.', ',', '?', '!', ')', ']', '}': 1358 end-- 1359 } 1360 } 1361 1362 sb.WriteString(text[:https]) 1363 sb.WriteString(underline) 1364 sb.WriteString(text[https:end]) 1365 sb.WriteString(reset) 1366 text = text[end:] 1367 } 1368 1369 sb.WriteString(text) 1370 return sb.String() 1371 } 1372 1373 func wrapWordsInString(text string, width int) []string { 1374 runs := []string{} 1375 1376 outer: 1377 for text != "" { 1378 i := 0 1379 x := 0 1380 wordEndI := 0 1381 1382 // Skip over any leading spaces 1383 for i < len(text) && text[i] == ' ' { 1384 i++ 1385 x++ 1386 } 1387 1388 // Find out how many words will fit in this run 1389 for i < len(text) { 1390 oldWordEndI := wordEndI 1391 wordStartI := i 1392 1393 // Find the end of the word 1394 for i < len(text) { 1395 c, width := utf8.DecodeRuneInString(text[i:]) 1396 if c == ' ' { 1397 break 1398 } 1399 i += width 1400 x += 1 // Naively assume that each unicode code point is a single column 1401 } 1402 wordEndI = i 1403 1404 // Split into a new run if this isn't the first word in the run and the end is past the width 1405 if wordStartI > 0 && x > width { 1406 runs = append(runs, text[:oldWordEndI]) 1407 text = text[wordStartI:] 1408 continue outer 1409 } 1410 1411 // Skip over any spaces after the word 1412 for i < len(text) && text[i] == ' ' { 1413 i++ 1414 x++ 1415 } 1416 } 1417 1418 // If we get here, this is the last run (i.e. everything fits) 1419 break 1420 } 1421 1422 // Remove any trailing spaces on the last run 1423 for len(text) > 0 && text[len(text)-1] == ' ' { 1424 text = text[:len(text)-1] 1425 } 1426 runs = append(runs, text) 1427 return runs 1428 } 1429 1430 type MsgDetail struct { 1431 SourceBefore string 1432 SourceMarked string 1433 SourceAfter string 1434 1435 Indent string 1436 Marker string 1437 Suggestion string 1438 1439 ContentAfter string 1440 1441 Path string 1442 Line int 1443 Column int 1444 } 1445 1446 // It's not common for large files to have many warnings. But when it happens, 1447 // we want to make sure that it's not too slow. Source code locations are 1448 // represented as byte offsets for compactness but transforming these to 1449 // line/column locations for warning messages requires scanning through the 1450 // file. A naive approach for this would cause O(n^2) scanning time for n 1451 // warnings distributed throughout the file. 1452 // 1453 // Warnings are typically generated sequentially as the file is scanned. So 1454 // one way of optimizing this is to just start scanning from where we left 1455 // off last time instead of always starting from the beginning of the file. 1456 // That's what this object does. 1457 // 1458 // Another option could be to eagerly populate an array of line/column offsets 1459 // and then use binary search for each query. This might slow down the common 1460 // case of a file with only at most a few warnings though, so think before 1461 // optimizing too much. Performance in the zero or one warning case is by far 1462 // the most important. 1463 type LineColumnTracker struct { 1464 contents string 1465 prettyPath string 1466 offset int32 1467 line int32 1468 lineStart int32 1469 lineEnd int32 1470 hasLineStart bool 1471 hasLineEnd bool 1472 hasSource bool 1473 } 1474 1475 func MakeLineColumnTracker(source *Source) LineColumnTracker { 1476 if source == nil { 1477 return LineColumnTracker{ 1478 hasSource: false, 1479 } 1480 } 1481 1482 return LineColumnTracker{ 1483 contents: source.Contents, 1484 prettyPath: source.PrettyPath, 1485 hasLineStart: true, 1486 hasSource: true, 1487 } 1488 } 1489 1490 func (tracker *LineColumnTracker) MsgData(r Range, text string) MsgData { 1491 return MsgData{ 1492 Text: text, 1493 Location: tracker.MsgLocationOrNil(r), 1494 } 1495 } 1496 1497 func (t *LineColumnTracker) scanTo(offset int32) { 1498 contents := t.contents 1499 i := t.offset 1500 1501 // Scan forward 1502 if i < offset { 1503 for { 1504 r, size := utf8.DecodeRuneInString(contents[i:]) 1505 i += int32(size) 1506 1507 switch r { 1508 case '\n': 1509 t.hasLineStart = true 1510 t.hasLineEnd = false 1511 t.lineStart = i 1512 if i == int32(size) || contents[i-int32(size)-1] != '\r' { 1513 t.line++ 1514 } 1515 1516 case '\r', '\u2028', '\u2029': 1517 t.hasLineStart = true 1518 t.hasLineEnd = false 1519 t.lineStart = i 1520 t.line++ 1521 } 1522 1523 if i >= offset { 1524 t.offset = i 1525 return 1526 } 1527 } 1528 } 1529 1530 // Scan backward 1531 if i > offset { 1532 for { 1533 r, size := utf8.DecodeLastRuneInString(contents[:i]) 1534 i -= int32(size) 1535 1536 switch r { 1537 case '\n': 1538 t.hasLineStart = false 1539 t.hasLineEnd = true 1540 t.lineEnd = i 1541 if i == 0 || contents[i-1] != '\r' { 1542 t.line-- 1543 } 1544 1545 case '\r', '\u2028', '\u2029': 1546 t.hasLineStart = false 1547 t.hasLineEnd = true 1548 t.lineEnd = i 1549 t.line-- 1550 } 1551 1552 if i <= offset { 1553 t.offset = i 1554 return 1555 } 1556 } 1557 } 1558 } 1559 1560 func (t *LineColumnTracker) computeLineAndColumn(offset int) (lineCount int, columnCount int, lineStart int, lineEnd int) { 1561 t.scanTo(int32(offset)) 1562 1563 // Scan for the start of the line 1564 if !t.hasLineStart { 1565 contents := t.contents 1566 i := t.offset 1567 for i > 0 { 1568 r, size := utf8.DecodeLastRuneInString(contents[:i]) 1569 if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' { 1570 break 1571 } 1572 i -= int32(size) 1573 } 1574 t.hasLineStart = true 1575 t.lineStart = i 1576 } 1577 1578 // Scan for the end of the line 1579 if !t.hasLineEnd { 1580 contents := t.contents 1581 i := t.offset 1582 n := int32(len(contents)) 1583 for i < n { 1584 r, size := utf8.DecodeRuneInString(contents[i:]) 1585 if r == '\n' || r == '\r' || r == '\u2028' || r == '\u2029' { 1586 break 1587 } 1588 i += int32(size) 1589 } 1590 t.hasLineEnd = true 1591 t.lineEnd = i 1592 } 1593 1594 return int(t.line), offset - int(t.lineStart), int(t.lineStart), int(t.lineEnd) 1595 } 1596 1597 func (tracker *LineColumnTracker) MsgLocationOrNil(r Range) *MsgLocation { 1598 if tracker == nil || !tracker.hasSource { 1599 return nil 1600 } 1601 1602 // Convert the index into a line and column number 1603 lineCount, columnCount, lineStart, lineEnd := tracker.computeLineAndColumn(int(r.Loc.Start)) 1604 1605 return &MsgLocation{ 1606 File: tracker.prettyPath, 1607 Line: lineCount + 1, // 0-based to 1-based 1608 Column: columnCount, 1609 Length: int(r.Len), 1610 LineText: tracker.contents[lineStart:lineEnd], 1611 } 1612 } 1613 1614 func detailStruct(data MsgData, terminalInfo TerminalInfo, maxMargin int) MsgDetail { 1615 // Only highlight the first line of the line text 1616 loc := *data.Location 1617 endOfFirstLine := len(loc.LineText) 1618 1619 // Note: This uses "IndexByte" because Go implements this with SIMD, which 1620 // can matter a lot for really long lines. Some people pass huge >100mb 1621 // minified files as line text for the log message. 1622 if i := strings.IndexByte(loc.LineText, '\n'); i >= 0 { 1623 endOfFirstLine = i 1624 } 1625 1626 firstLine := loc.LineText[:endOfFirstLine] 1627 afterFirstLine := loc.LineText[endOfFirstLine:] 1628 if afterFirstLine != "" && !strings.HasSuffix(afterFirstLine, "\n") { 1629 afterFirstLine += "\n" 1630 } 1631 1632 // Clamp values in range 1633 if loc.Line < 0 { 1634 loc.Line = 0 1635 } 1636 if loc.Column < 0 { 1637 loc.Column = 0 1638 } 1639 if loc.Length < 0 { 1640 loc.Length = 0 1641 } 1642 if loc.Column > endOfFirstLine { 1643 loc.Column = endOfFirstLine 1644 } 1645 if loc.Length > endOfFirstLine-loc.Column { 1646 loc.Length = endOfFirstLine - loc.Column 1647 } 1648 1649 spacesPerTab := 2 1650 lineText := renderTabStops(firstLine, spacesPerTab) 1651 textUpToLoc := renderTabStops(firstLine[:loc.Column], spacesPerTab) 1652 markerStart := len(textUpToLoc) 1653 markerEnd := markerStart 1654 indent := strings.Repeat(" ", estimateWidthInTerminal(textUpToLoc)) 1655 marker := "^" 1656 1657 // Extend markers to cover the full range of the error 1658 if loc.Length > 0 { 1659 markerEnd = len(renderTabStops(firstLine[:loc.Column+loc.Length], spacesPerTab)) 1660 } 1661 1662 // Clip the marker to the bounds of the line 1663 if markerStart > len(lineText) { 1664 markerStart = len(lineText) 1665 } 1666 if markerEnd > len(lineText) { 1667 markerEnd = len(lineText) 1668 } 1669 if markerEnd < markerStart { 1670 markerEnd = markerStart 1671 } 1672 1673 // Trim the line to fit the terminal width 1674 width := terminalInfo.Width 1675 if width < 1 { 1676 width = defaultTerminalWidth 1677 } 1678 width -= maxMargin + extraMarginChars 1679 if width < 1 { 1680 width = 1 1681 } 1682 if loc.Column == endOfFirstLine { 1683 // If the marker is at the very end of the line, the marker will be a "^" 1684 // character that extends one column past the end of the line. In this case 1685 // we should reserve a column at the end so the marker doesn't wrap. 1686 width -= 1 1687 } 1688 if len(lineText) > width { 1689 // Try to center the error 1690 sliceStart := (markerStart + markerEnd - width) / 2 1691 if sliceStart > markerStart-width/5 { 1692 sliceStart = markerStart - width/5 1693 } 1694 if sliceStart < 0 { 1695 sliceStart = 0 1696 } 1697 if sliceStart > len(lineText)-width { 1698 sliceStart = len(lineText) - width 1699 } 1700 sliceEnd := sliceStart + width 1701 1702 // Slice the line 1703 slicedLine := lineText[sliceStart:sliceEnd] 1704 markerStart -= sliceStart 1705 markerEnd -= sliceStart 1706 if markerStart < 0 { 1707 markerStart = 0 1708 } 1709 if markerEnd > len(slicedLine) { 1710 markerEnd = len(slicedLine) 1711 } 1712 1713 // Truncate the ends with "..." 1714 if len(slicedLine) > 3 && sliceStart > 0 { 1715 slicedLine = "..." + slicedLine[3:] 1716 if markerStart < 3 { 1717 markerStart = 3 1718 } 1719 } 1720 if len(slicedLine) > 3 && sliceEnd < len(lineText) { 1721 slicedLine = slicedLine[:len(slicedLine)-3] + "..." 1722 if markerEnd > len(slicedLine)-3 { 1723 markerEnd = len(slicedLine) - 3 1724 } 1725 if markerEnd < markerStart { 1726 markerEnd = markerStart 1727 } 1728 } 1729 1730 // Now we can compute the indent 1731 lineText = slicedLine 1732 indent = strings.Repeat(" ", estimateWidthInTerminal(lineText[:markerStart])) 1733 } 1734 1735 // If marker is still multi-character after clipping, make the marker wider 1736 if markerEnd-markerStart > 1 { 1737 marker = strings.Repeat("~", estimateWidthInTerminal(lineText[markerStart:markerEnd])) 1738 } 1739 1740 // Put a margin before the marker indent 1741 margin := marginWithLineText(maxMargin, loc.Line) 1742 1743 return MsgDetail{ 1744 Path: loc.File, 1745 Line: loc.Line, 1746 Column: loc.Column, 1747 1748 SourceBefore: margin + lineText[:markerStart], 1749 SourceMarked: lineText[markerStart:markerEnd], 1750 SourceAfter: lineText[markerEnd:], 1751 1752 Indent: indent, 1753 Marker: marker, 1754 Suggestion: loc.Suggestion, 1755 1756 ContentAfter: afterFirstLine, 1757 } 1758 } 1759 1760 // Estimate the number of columns this string will take when printed 1761 func estimateWidthInTerminal(text string) int { 1762 // For now just assume each code point is one column. This is wrong but is 1763 // less wrong than assuming each code unit is one column. 1764 width := 0 1765 for text != "" { 1766 c, size := utf8.DecodeRuneInString(text) 1767 text = text[size:] 1768 1769 // Ignore the Zero Width No-Break Space character (UTF-8 BOM) 1770 if c != 0xFEFF { 1771 width++ 1772 } 1773 } 1774 return width 1775 } 1776 1777 func renderTabStops(withTabs string, spacesPerTab int) string { 1778 if !strings.ContainsRune(withTabs, '\t') { 1779 return withTabs 1780 } 1781 1782 withoutTabs := strings.Builder{} 1783 count := 0 1784 1785 for _, c := range withTabs { 1786 if c == '\t' { 1787 spaces := spacesPerTab - count%spacesPerTab 1788 for i := 0; i < spaces; i++ { 1789 withoutTabs.WriteRune(' ') 1790 count++ 1791 } 1792 } else { 1793 withoutTabs.WriteRune(c) 1794 count++ 1795 } 1796 } 1797 1798 return withoutTabs.String() 1799 } 1800 1801 func (log Log) AddError(tracker *LineColumnTracker, r Range, text string) { 1802 log.AddMsg(Msg{ 1803 Kind: Error, 1804 Data: tracker.MsgData(r, text), 1805 }) 1806 } 1807 1808 func (log Log) AddID(id MsgID, kind MsgKind, tracker *LineColumnTracker, r Range, text string) { 1809 if override, ok := allowOverride(log.Overrides, id, kind); ok { 1810 log.AddMsg(Msg{ 1811 ID: id, 1812 Kind: override, 1813 Data: tracker.MsgData(r, text), 1814 }) 1815 } 1816 } 1817 1818 func (log Log) AddErrorWithNotes(tracker *LineColumnTracker, r Range, text string, notes []MsgData) { 1819 log.AddMsg(Msg{ 1820 Kind: Error, 1821 Data: tracker.MsgData(r, text), 1822 Notes: notes, 1823 }) 1824 } 1825 1826 func (log Log) AddIDWithNotes(id MsgID, kind MsgKind, tracker *LineColumnTracker, r Range, text string, notes []MsgData) { 1827 if override, ok := allowOverride(log.Overrides, id, kind); ok { 1828 log.AddMsg(Msg{ 1829 ID: id, 1830 Kind: override, 1831 Data: tracker.MsgData(r, text), 1832 Notes: notes, 1833 }) 1834 } 1835 } 1836 1837 func (log Log) AddMsgID(id MsgID, msg Msg) { 1838 if override, ok := allowOverride(log.Overrides, id, msg.Kind); ok { 1839 msg.ID = id 1840 msg.Kind = override 1841 log.AddMsg(msg) 1842 } 1843 } 1844 1845 func allowOverride(overrides map[MsgID]LogLevel, id MsgID, kind MsgKind) (MsgKind, bool) { 1846 if logLevel, ok := overrides[id]; ok { 1847 switch logLevel { 1848 case LevelVerbose: 1849 return Verbose, true 1850 case LevelDebug: 1851 return Debug, true 1852 case LevelInfo: 1853 return Info, true 1854 case LevelWarning: 1855 return Warning, true 1856 case LevelError: 1857 return Error, true 1858 default: 1859 // Setting the log level to "silent" silences this log message 1860 return MsgKind(0), false 1861 } 1862 } 1863 return kind, true 1864 } 1865 1866 type StringInJSTableEntry struct { 1867 innerLine int32 1868 innerColumn int32 1869 innerLoc Loc 1870 outerLoc Loc 1871 } 1872 1873 // For Yarn PnP we sometimes parse JSON embedded in a JS string. This generates 1874 // a table that remaps locations inside the embedded JSON string literal into 1875 // locations in the actual JS file, which makes them easier to understand. 1876 func GenerateStringInJSTable(outerContents string, outerStringLiteralLoc Loc, innerContents string) (table []StringInJSTableEntry) { 1877 i := int32(0) 1878 n := int32(len(innerContents)) 1879 line := int32(1) 1880 column := int32(0) 1881 loc := Loc{Start: outerStringLiteralLoc.Start + 1} 1882 1883 for i < n { 1884 // Ignore line continuations. A line continuation is not an escaped newline. 1885 for { 1886 if c, _ := utf8.DecodeRuneInString(outerContents[loc.Start:]); c != '\\' { 1887 break 1888 } 1889 c, width := utf8.DecodeRuneInString(outerContents[loc.Start+1:]) 1890 switch c { 1891 case '\n', '\r', '\u2028', '\u2029': 1892 loc.Start += 1 + int32(width) 1893 if c == '\r' && outerContents[loc.Start] == '\n' { 1894 // Make sure Windows CRLF counts as a single newline 1895 loc.Start++ 1896 } 1897 continue 1898 } 1899 break 1900 } 1901 1902 c, width := utf8.DecodeRuneInString(innerContents[i:]) 1903 1904 // Compress the table using run-length encoding 1905 table = append(table, StringInJSTableEntry{innerLine: line, innerColumn: column, innerLoc: Loc{Start: i}, outerLoc: loc}) 1906 if len(table) > 1 { 1907 if last := table[len(table)-2]; line == last.innerLine && loc.Start-column == last.outerLoc.Start-last.innerColumn { 1908 table = table[:len(table)-1] 1909 } 1910 } 1911 1912 // Advance the inner line/column 1913 switch c { 1914 case '\n', '\r', '\u2028', '\u2029': 1915 line++ 1916 column = 0 1917 1918 // Handle newlines on Windows 1919 if c == '\r' && i+1 < n && innerContents[i+1] == '\n' { 1920 i++ 1921 } 1922 1923 default: 1924 column += int32(width) 1925 } 1926 i += int32(width) 1927 1928 // Advance the outer loc, assuming the string syntax is already valid 1929 c, width = utf8.DecodeRuneInString(outerContents[loc.Start:]) 1930 if c == '\r' && outerContents[loc.Start+1] == '\n' { 1931 // Handle newlines on Windows in template literal strings 1932 loc.Start += 2 1933 } else if c != '\\' { 1934 loc.Start += int32(width) 1935 } else { 1936 // Handle an escape sequence 1937 c, width = utf8.DecodeRuneInString(outerContents[loc.Start+1:]) 1938 switch c { 1939 case 'x': 1940 // 2-digit hexadecimal 1941 loc.Start += 1 + 2 1942 1943 case 'u': 1944 loc.Start++ 1945 if outerContents[loc.Start] == '{' { 1946 // Variable-length 1947 for outerContents[loc.Start] != '}' { 1948 loc.Start++ 1949 } 1950 loc.Start++ 1951 } else { 1952 // Fixed-length 1953 loc.Start += 4 1954 } 1955 1956 case '\n', '\r', '\u2028', '\u2029': 1957 // This will be handled by the next iteration 1958 break 1959 1960 default: 1961 loc.Start += 1 + int32(width) 1962 } 1963 } 1964 } 1965 1966 return 1967 } 1968 1969 func RemapStringInJSLoc(table []StringInJSTableEntry, innerLoc Loc) Loc { 1970 count := len(table) 1971 index := 0 1972 1973 // Binary search to find the previous entry 1974 for count > 0 { 1975 step := count / 2 1976 i := index + step 1977 if i+1 < len(table) { 1978 if entry := table[i+1]; entry.innerLoc.Start < innerLoc.Start { 1979 index = i + 1 1980 count -= step + 1 1981 continue 1982 } 1983 } 1984 count = step 1985 } 1986 1987 entry := table[index] 1988 entry.outerLoc.Start += innerLoc.Start - entry.innerLoc.Start // Undo run-length compression 1989 return entry.outerLoc 1990 } 1991 1992 func NewStringInJSLog(log Log, outerTracker *LineColumnTracker, table []StringInJSTableEntry) Log { 1993 oldAddMsg := log.AddMsg 1994 1995 remapLineAndColumnToLoc := func(line int32, column int32) Loc { 1996 count := len(table) 1997 index := 0 1998 1999 // Binary search to find the previous entry 2000 for count > 0 { 2001 step := count / 2 2002 i := index + step 2003 if i+1 < len(table) { 2004 if entry := table[i+1]; entry.innerLine < line || (entry.innerLine == line && entry.innerColumn < column) { 2005 index = i + 1 2006 count -= step + 1 2007 continue 2008 } 2009 } 2010 count = step 2011 } 2012 2013 entry := table[index] 2014 entry.outerLoc.Start += column - entry.innerColumn // Undo run-length compression 2015 return entry.outerLoc 2016 } 2017 2018 remapData := func(data MsgData) MsgData { 2019 if data.Location == nil { 2020 return data 2021 } 2022 2023 // Generate a range in the outer source using the line/column/length in the inner source 2024 r := Range{Loc: remapLineAndColumnToLoc(int32(data.Location.Line), int32(data.Location.Column))} 2025 if data.Location.Length != 0 { 2026 r.Len = remapLineAndColumnToLoc(int32(data.Location.Line), int32(data.Location.Column+data.Location.Length)).Start - r.Loc.Start 2027 } 2028 2029 // Use that range to look up the line in the outer source 2030 location := outerTracker.MsgData(r, data.Text).Location 2031 location.Suggestion = data.Location.Suggestion 2032 data.Location = location 2033 return data 2034 } 2035 2036 log.AddMsg = func(msg Msg) { 2037 msg.Data = remapData(msg.Data) 2038 for i, note := range msg.Notes { 2039 msg.Notes[i] = remapData(note) 2040 } 2041 oldAddMsg(msg) 2042 } 2043 2044 return log 2045 }