github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/packages/package.go (about) 1 /* Copyright 2017 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package packages 17 18 import ( 19 "fmt" 20 "log" 21 "path" 22 "sort" 23 "strings" 24 25 "github.com/bazelbuild/bazel-gazelle/internal/config" 26 "github.com/bazelbuild/bazel-gazelle/internal/pathtools" 27 ) 28 29 // Package contains metadata about a Go package extracted from a directory. 30 // It fills a similar role to go/build.Package, but it separates files by 31 // target instead of by type, and it supports multiple platforms. 32 type Package struct { 33 // Name is the symbol found in package declarations of the .go files in 34 // the package. It does not include the "_test" suffix from external tests. 35 Name string 36 37 // Dir is an absolute path to the directory that contains the package. 38 Dir string 39 40 // Rel is the relative path to the package directory from the repository 41 // root. If the directory is the repository root itself, Rel is empty. 42 // Components in Rel are separated with slashes. 43 Rel string 44 45 // ImportPath is the string used to import this package in Go. 46 ImportPath string 47 48 Library, Binary, Test, XTest GoTarget 49 Proto ProtoTarget 50 51 HasTestdata bool 52 } 53 54 // GoTarget contains metadata about a buildable Go target in a package. 55 type GoTarget struct { 56 Sources, Imports PlatformStrings 57 COpts, CLinkOpts PlatformStrings 58 Cgo bool 59 } 60 61 // ProtoTarget contains metadata about proto files in a package. 62 type ProtoTarget struct { 63 Sources, Imports PlatformStrings 64 HasServices bool 65 66 // HasPbGo indicates whether unexcluded .pb.go files are present in the 67 // same package. They will not be in this target's sources. 68 HasPbGo bool 69 } 70 71 // PlatformStrings contains a set of strings associated with a buildable 72 // Go target in a package. This is used to store source file names, 73 // import paths, and flags. 74 // 75 // Strings are stored in four sets: generic strings, OS-specific strings, 76 // arch-specific strings, and OS-and-arch-specific strings. A string may not 77 // be duplicated within a list or across sets; however, a string may appear 78 // in more than one list within a set (e.g., in "linux" and "windows" within 79 // the OS set). Strings within each list should be sorted, though this may 80 // not be relied upon. 81 type PlatformStrings struct { 82 // Generic is a list of strings not specific to any platform. 83 Generic []string 84 85 // OS is a map from OS name (anything in config.KnownOSs) to 86 // OS-specific strings. 87 OS map[string][]string 88 89 // Arch is a map from architecture name (anything in config.KnownArchs) to 90 // architecture-specific strings. 91 Arch map[string][]string 92 93 // Platform is a map from platforms to OS and architecture-specific strings. 94 Platform map[config.Platform][]string 95 } 96 97 // IsCommand returns true if the package name is "main". 98 func (p *Package) IsCommand() bool { 99 return p.Name == "main" 100 } 101 102 // EmptyPackage returns an empty package. The package name and import path 103 // are inferred from the directory name and configuration. This is useful 104 // for deleting rules in directories which no longer have source files. 105 func EmptyPackage(c *config.Config, dir, rel string) *Package { 106 packageName := pathtools.RelBaseName(rel, c.GoPrefix, c.RepoRoot) 107 pb := packageBuilder{ 108 name: packageName, 109 dir: dir, 110 rel: rel, 111 } 112 pb.inferImportPath(c) 113 return pb.build() 114 } 115 116 func (t *GoTarget) HasGo() bool { 117 return t.Sources.HasGo() 118 } 119 120 func (t *ProtoTarget) HasProto() bool { 121 return !t.Sources.IsEmpty() 122 } 123 124 func (ps *PlatformStrings) HasGo() bool { 125 return ps.firstGoFile() != "" 126 } 127 128 func (ps *PlatformStrings) IsEmpty() bool { 129 return len(ps.Generic) == 0 && len(ps.OS) == 0 && len(ps.Arch) == 0 && len(ps.Platform) == 0 130 } 131 132 func (ps *PlatformStrings) firstGoFile() string { 133 for _, f := range ps.Generic { 134 if strings.HasSuffix(f, ".go") { 135 return f 136 } 137 } 138 for _, fs := range ps.OS { 139 for _, f := range fs { 140 if strings.HasSuffix(f, ".go") { 141 return f 142 } 143 } 144 } 145 for _, fs := range ps.Arch { 146 for _, f := range fs { 147 if strings.HasSuffix(f, ".go") { 148 return f 149 } 150 } 151 } 152 for _, fs := range ps.Platform { 153 for _, f := range fs { 154 if strings.HasSuffix(f, ".go") { 155 return f 156 } 157 } 158 } 159 return "" 160 } 161 162 type packageBuilder struct { 163 name, dir, rel string 164 library, binary, test, xtest goTargetBuilder 165 proto protoTargetBuilder 166 hasTestdata bool 167 importPath, importPathFile string 168 } 169 170 type goTargetBuilder struct { 171 sources, imports, copts, clinkopts platformStringsBuilder 172 cgo bool 173 } 174 175 type protoTargetBuilder struct { 176 sources, imports platformStringsBuilder 177 hasServices, hasPbGo bool 178 } 179 180 type platformStringsBuilder struct { 181 strs map[string]platformStringInfo 182 } 183 184 type platformStringInfo struct { 185 set platformStringSet 186 oss map[string]bool 187 archs map[string]bool 188 platforms map[config.Platform]bool 189 } 190 191 type platformStringSet int 192 193 const ( 194 genericSet platformStringSet = iota 195 osSet 196 archSet 197 platformSet 198 ) 199 200 // addFile adds the file described by "info" to a target in the package "p" if 201 // the file is buildable. 202 // 203 // "cgo" tells whether any ".go" file in the package contains cgo code. This 204 // affects whether C files are added to targets. 205 // 206 // An error is returned if a file is buildable but invalid (for example, a 207 // test .go file containing cgo code). Files that are not buildable will not 208 // be added to any target (for example, .txt files). 209 func (pb *packageBuilder) addFile(c *config.Config, info fileInfo, cgo bool) error { 210 switch { 211 case info.category == ignoredExt || info.category == unsupportedExt || 212 !cgo && (info.category == cExt || info.category == csExt) || 213 c.ProtoMode == config.DisableProtoMode && info.category == protoExt: 214 return nil 215 case info.isXTest: 216 if info.isCgo { 217 return fmt.Errorf("%s: use of cgo in test not supported", info.path) 218 } 219 pb.xtest.addFile(c, info) 220 case info.isTest: 221 if info.isCgo { 222 return fmt.Errorf("%s: use of cgo in test not supported", info.path) 223 } 224 pb.test.addFile(c, info) 225 case info.category == protoExt: 226 pb.proto.addFile(c, info) 227 default: 228 pb.library.addFile(c, info) 229 } 230 if strings.HasSuffix(info.name, ".pb.go") { 231 pb.proto.hasPbGo = true 232 } 233 234 if info.importPath != "" { 235 if pb.importPath == "" { 236 pb.importPath = info.importPath 237 pb.importPathFile = info.path 238 } else if pb.importPath != info.importPath { 239 return fmt.Errorf("found import comments %q (%s) and %q (%s)", pb.importPath, pb.importPathFile, info.importPath, info.path) 240 } 241 } 242 243 return nil 244 } 245 246 // isBuildable returns true if anything in the package is buildable. 247 // This is true if the package has Go code that satisfies build constraints 248 // on any platform or has proto files not in legacy mode. 249 func (pb *packageBuilder) isBuildable(c *config.Config) bool { 250 return pb.firstGoFile() != "" || 251 len(pb.proto.sources.strs) > 0 && c.ProtoMode == config.DefaultProtoMode 252 } 253 254 // firstGoFile returns the name of a .go file if the package contains at least 255 // one .go file, or "" otherwise. 256 func (pb *packageBuilder) firstGoFile() string { 257 goSrcs := []platformStringsBuilder{ 258 pb.library.sources, 259 pb.binary.sources, 260 pb.test.sources, 261 pb.xtest.sources, 262 } 263 for _, sb := range goSrcs { 264 if sb.strs != nil { 265 for s, _ := range sb.strs { 266 if strings.HasSuffix(s, ".go") { 267 return s 268 } 269 } 270 } 271 } 272 return "" 273 } 274 275 func (pb *packageBuilder) inferImportPath(c *config.Config) error { 276 if pb.importPath != "" { 277 log.Panic("importPath already set") 278 } 279 if pb.rel == c.GoPrefixRel { 280 if c.GoPrefix == "" { 281 return fmt.Errorf("in directory %q, prefix is empty, so importpath would be empty for rules. Set a prefix with a '# gazelle:prefix' comment or with -go_prefix on the command line.", pb.dir) 282 } 283 pb.importPath = c.GoPrefix 284 } else { 285 fromPrefixRel := strings.TrimPrefix(pb.rel, c.GoPrefixRel+"/") 286 pb.importPath = path.Join(c.GoPrefix, fromPrefixRel) 287 } 288 return nil 289 } 290 291 func (pb *packageBuilder) build() *Package { 292 return &Package{ 293 Name: pb.name, 294 Dir: pb.dir, 295 Rel: pb.rel, 296 ImportPath: pb.importPath, 297 Library: pb.library.build(), 298 Binary: pb.binary.build(), 299 Test: pb.test.build(), 300 XTest: pb.xtest.build(), 301 Proto: pb.proto.build(), 302 HasTestdata: pb.hasTestdata, 303 } 304 } 305 306 func (tb *goTargetBuilder) addFile(c *config.Config, info fileInfo) { 307 tb.cgo = tb.cgo || info.isCgo 308 add := getPlatformStringsAddFunction(c, info, nil) 309 add(&tb.sources, info.name) 310 add(&tb.imports, info.imports...) 311 for _, copts := range info.copts { 312 optAdd := add 313 if len(copts.tags) > 0 { 314 optAdd = getPlatformStringsAddFunction(c, info, copts.tags) 315 } 316 optAdd(&tb.copts, copts.opts) 317 } 318 for _, clinkopts := range info.clinkopts { 319 optAdd := add 320 if len(clinkopts.tags) > 0 { 321 optAdd = getPlatformStringsAddFunction(c, info, clinkopts.tags) 322 } 323 optAdd(&tb.clinkopts, clinkopts.opts) 324 } 325 } 326 327 func (tb *goTargetBuilder) build() GoTarget { 328 return GoTarget{ 329 Sources: tb.sources.build(), 330 Imports: tb.imports.build(), 331 COpts: tb.copts.build(), 332 CLinkOpts: tb.clinkopts.build(), 333 Cgo: tb.cgo, 334 } 335 } 336 337 func (tb *protoTargetBuilder) addFile(c *config.Config, info fileInfo) { 338 add := getPlatformStringsAddFunction(c, info, nil) 339 add(&tb.sources, info.name) 340 add(&tb.imports, info.imports...) 341 tb.hasServices = tb.hasServices || info.hasServices 342 } 343 344 func (tb *protoTargetBuilder) build() ProtoTarget { 345 return ProtoTarget{ 346 Sources: tb.sources.build(), 347 Imports: tb.imports.build(), 348 HasServices: tb.hasServices, 349 HasPbGo: tb.hasPbGo, 350 } 351 } 352 353 // getPlatformStringsAddFunction returns a function used to add strings to 354 // a *platformStringsBuilder under the same set of constraints. This is a 355 // performance optimization to avoid evaluating constraints repeatedly. 356 func getPlatformStringsAddFunction(c *config.Config, info fileInfo, cgoTags tagLine) func(sb *platformStringsBuilder, ss ...string) { 357 isOSSpecific, isArchSpecific := isOSArchSpecific(info, cgoTags) 358 359 switch { 360 case !isOSSpecific && !isArchSpecific: 361 if checkConstraints(c, "", "", info.goos, info.goarch, info.tags, cgoTags) { 362 return func(sb *platformStringsBuilder, ss ...string) { 363 for _, s := range ss { 364 sb.addGenericString(s) 365 } 366 } 367 } 368 369 case isOSSpecific && !isArchSpecific: 370 var osMatch []string 371 for _, os := range config.KnownOSs { 372 if checkConstraints(c, os, "", info.goos, info.goarch, info.tags, cgoTags) { 373 osMatch = append(osMatch, os) 374 } 375 } 376 if len(osMatch) > 0 { 377 return func(sb *platformStringsBuilder, ss ...string) { 378 for _, s := range ss { 379 sb.addOSString(s, osMatch) 380 } 381 } 382 } 383 384 case !isOSSpecific && isArchSpecific: 385 var archMatch []string 386 for _, arch := range config.KnownArchs { 387 if checkConstraints(c, "", arch, info.goos, info.goarch, info.tags, cgoTags) { 388 archMatch = append(archMatch, arch) 389 } 390 } 391 if len(archMatch) > 0 { 392 return func(sb *platformStringsBuilder, ss ...string) { 393 for _, s := range ss { 394 sb.addArchString(s, archMatch) 395 } 396 } 397 } 398 399 default: 400 var platformMatch []config.Platform 401 for _, platform := range config.KnownPlatforms { 402 if checkConstraints(c, platform.OS, platform.Arch, info.goos, info.goarch, info.tags, cgoTags) { 403 platformMatch = append(platformMatch, platform) 404 } 405 } 406 if len(platformMatch) > 0 { 407 return func(sb *platformStringsBuilder, ss ...string) { 408 for _, s := range ss { 409 sb.addPlatformString(s, platformMatch) 410 } 411 } 412 } 413 } 414 415 return func(_ *platformStringsBuilder, _ ...string) {} 416 } 417 418 func (sb *platformStringsBuilder) addGenericString(s string) { 419 if sb.strs == nil { 420 sb.strs = make(map[string]platformStringInfo) 421 } 422 sb.strs[s] = platformStringInfo{set: genericSet} 423 } 424 425 func (sb *platformStringsBuilder) addOSString(s string, oss []string) { 426 if sb.strs == nil { 427 sb.strs = make(map[string]platformStringInfo) 428 } 429 si, ok := sb.strs[s] 430 if !ok { 431 si.set = osSet 432 si.oss = make(map[string]bool) 433 } 434 switch si.set { 435 case genericSet: 436 return 437 case osSet: 438 for _, os := range oss { 439 si.oss[os] = true 440 } 441 default: 442 si.convertToPlatforms() 443 for _, os := range oss { 444 for _, arch := range config.KnownOSArchs[os] { 445 si.platforms[config.Platform{OS: os, Arch: arch}] = true 446 } 447 } 448 } 449 sb.strs[s] = si 450 } 451 452 func (sb *platformStringsBuilder) addArchString(s string, archs []string) { 453 if sb.strs == nil { 454 sb.strs = make(map[string]platformStringInfo) 455 } 456 si, ok := sb.strs[s] 457 if !ok { 458 si.set = archSet 459 si.archs = make(map[string]bool) 460 } 461 switch si.set { 462 case genericSet: 463 return 464 case archSet: 465 for _, arch := range archs { 466 si.archs[arch] = true 467 } 468 default: 469 si.convertToPlatforms() 470 for _, arch := range archs { 471 for _, os := range config.KnownArchOSs[arch] { 472 si.platforms[config.Platform{OS: os, Arch: arch}] = true 473 } 474 } 475 } 476 sb.strs[s] = si 477 } 478 479 func (sb *platformStringsBuilder) addPlatformString(s string, platforms []config.Platform) { 480 if sb.strs == nil { 481 sb.strs = make(map[string]platformStringInfo) 482 } 483 si, ok := sb.strs[s] 484 if !ok { 485 si.set = platformSet 486 si.platforms = make(map[config.Platform]bool) 487 } 488 switch si.set { 489 case genericSet: 490 return 491 default: 492 si.convertToPlatforms() 493 for _, p := range platforms { 494 si.platforms[p] = true 495 } 496 } 497 sb.strs[s] = si 498 } 499 500 func (sb *platformStringsBuilder) build() PlatformStrings { 501 var ps PlatformStrings 502 for s, si := range sb.strs { 503 switch si.set { 504 case genericSet: 505 ps.Generic = append(ps.Generic, s) 506 case osSet: 507 if ps.OS == nil { 508 ps.OS = make(map[string][]string) 509 } 510 for os, _ := range si.oss { 511 ps.OS[os] = append(ps.OS[os], s) 512 } 513 case archSet: 514 if ps.Arch == nil { 515 ps.Arch = make(map[string][]string) 516 } 517 for arch, _ := range si.archs { 518 ps.Arch[arch] = append(ps.Arch[arch], s) 519 } 520 case platformSet: 521 if ps.Platform == nil { 522 ps.Platform = make(map[config.Platform][]string) 523 } 524 for p, _ := range si.platforms { 525 ps.Platform[p] = append(ps.Platform[p], s) 526 } 527 } 528 } 529 sort.Strings(ps.Generic) 530 if ps.OS != nil { 531 for _, ss := range ps.OS { 532 sort.Strings(ss) 533 } 534 } 535 if ps.Arch != nil { 536 for _, ss := range ps.Arch { 537 sort.Strings(ss) 538 } 539 } 540 if ps.Platform != nil { 541 for _, ss := range ps.Platform { 542 sort.Strings(ss) 543 } 544 } 545 return ps 546 } 547 548 func (si *platformStringInfo) convertToPlatforms() { 549 switch si.set { 550 case genericSet: 551 log.Panic("cannot convert generic string to platforms") 552 case platformSet: 553 return 554 case osSet: 555 si.set = platformSet 556 si.platforms = make(map[config.Platform]bool) 557 for os, _ := range si.oss { 558 for _, arch := range config.KnownOSArchs[os] { 559 si.platforms[config.Platform{OS: os, Arch: arch}] = true 560 } 561 } 562 si.oss = nil 563 case archSet: 564 si.set = platformSet 565 si.platforms = make(map[config.Platform]bool) 566 for arch, _ := range si.archs { 567 for _, os := range config.KnownArchOSs[arch] { 568 si.platforms[config.Platform{OS: os, Arch: arch}] = true 569 } 570 } 571 si.archs = nil 572 } 573 } 574 575 // MapSlice applies a function that processes slices of strings to the strings 576 // in "ps" and returns a new PlatformStrings with the results. 577 func (ps *PlatformStrings) MapSlice(f func([]string) ([]string, error)) (PlatformStrings, []error) { 578 var errors []error 579 580 mapSlice := func(ss []string) []string { 581 rs, err := f(ss) 582 if err != nil { 583 errors = append(errors, err) 584 return nil 585 } 586 return rs 587 } 588 589 mapStringMap := func(m map[string][]string) map[string][]string { 590 if m == nil { 591 return nil 592 } 593 rm := make(map[string][]string) 594 for k, ss := range m { 595 ss = mapSlice(ss) 596 if len(ss) > 0 { 597 rm[k] = ss 598 } 599 } 600 if len(rm) == 0 { 601 return nil 602 } 603 return rm 604 } 605 606 mapPlatformMap := func(m map[config.Platform][]string) map[config.Platform][]string { 607 if m == nil { 608 return nil 609 } 610 rm := make(map[config.Platform][]string) 611 for k, ss := range m { 612 ss = mapSlice(ss) 613 if len(ss) > 0 { 614 rm[k] = ss 615 } 616 } 617 if len(rm) == 0 { 618 return nil 619 } 620 return rm 621 } 622 623 result := PlatformStrings{ 624 Generic: mapSlice(ps.Generic), 625 OS: mapStringMap(ps.OS), 626 Arch: mapStringMap(ps.Arch), 627 Platform: mapPlatformMap(ps.Platform), 628 } 629 return result, errors 630 }