github.com/googleapis/api-linter@v1.65.2/lint/problem.go (about)

     1  // Copyright 2019 Google LLC
     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  // 		https://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  package lint
    16  
    17  import (
    18  	"encoding/json"
    19  
    20  	"github.com/jhump/protoreflect/desc"
    21  	dpb "google.golang.org/protobuf/types/descriptorpb"
    22  )
    23  
    24  // Problem contains information about a result produced by an API Linter.
    25  //
    26  // All rules return []Problem. Most lint rules return 0 or 1 problems, but
    27  // occasionally there are rules that may return more than one.
    28  type Problem struct {
    29  	// Message provides a short description of the problem.
    30  	// This should be no more than a single sentence.
    31  	Message string
    32  
    33  	// Suggestion provides a suggested fix, if applicable.
    34  	//
    35  	// This integrates with certain IDEs to provide "push-button" fixes,
    36  	// so these need to be machine-readable, not just human-readable.
    37  	// Additionally, when setting `Suggestion`, one should almost always set
    38  	// `Location` also, to ensure that the text being replaced is sufficiently
    39  	// precise.
    40  	Suggestion string
    41  
    42  	// Descriptor provides the descriptor related to the problem. This must be
    43  	// set on every Problem.
    44  	//
    45  	// If `Location` is not specified, then the starting location of
    46  	// the descriptor is used as the location of the problem.
    47  	Descriptor desc.Descriptor
    48  
    49  	// Location provides the location of the problem.
    50  	//
    51  	// If unset, this defaults to the value of `Descriptor.GetSourceInfo()`.
    52  	// This should almost always be set if `Suggestion` is set. The best way to
    53  	// do this is by using the helper methods in `location.go`.
    54  	Location *dpb.SourceCodeInfo_Location
    55  
    56  	// RuleID provides the ID of the rule that this problem belongs to.
    57  	// DO NOT SET: The linter sets this automatically.
    58  	RuleID RuleName // FIXME: Make this private (cmd/summary_cli.go is the challenge).
    59  
    60  	// The category for this problem, based on user configuration.
    61  	category string
    62  
    63  	//lint:ignore U1000 ignored via golint previously
    64  	noPositional struct{}
    65  }
    66  
    67  // MarshalJSON defines how to represent a Problem in JSON.
    68  func (p Problem) MarshalJSON() ([]byte, error) {
    69  	return json.Marshal(p.marshal())
    70  }
    71  
    72  // MarshalYAML defines how to represent a Problem in YAML.
    73  func (p Problem) MarshalYAML() (interface{}, error) {
    74  	return p.marshal(), nil
    75  }
    76  
    77  // Marshal defines how to represent a serialized Problem.
    78  func (p Problem) marshal() interface{} {
    79  	// The descriptor is always set, and location may be set.
    80  	// If they are both set, prefer the location.
    81  	loc := p.Location
    82  	if loc == nil && p.Descriptor != nil {
    83  		loc = p.Descriptor.GetSourceInfo()
    84  	}
    85  
    86  	// Return a marshal-able structure.
    87  	return struct {
    88  		Message    string       `json:"message" yaml:"message"`
    89  		Suggestion string       `json:"suggestion,omitempty" yaml:"suggestion,omitempty"`
    90  		Location   fileLocation `json:"location" yaml:"location"`
    91  		RuleID     RuleName     `json:"rule_id" yaml:"rule_id"`
    92  		RuleDocURI string       `json:"rule_doc_uri" yaml:"rule_doc_uri"`
    93  		Category   string       `json:"category,omitempty" yaml:"category,omitempty"`
    94  	}{
    95  		p.Message,
    96  		p.Suggestion,
    97  		fileLocationFromPBLocation(loc, p.Descriptor),
    98  		p.RuleID,
    99  		p.GetRuleURI(),
   100  		p.category,
   101  	}
   102  }
   103  
   104  // GetRuleURI returns a URI to learn more about the problem.
   105  func (p Problem) GetRuleURI() string {
   106  	return getRuleURL(string(p.RuleID), ruleURLMappings)
   107  }
   108  
   109  // position describes a one-based position in a source code file.
   110  // They are one-indexed, as a human counts lines or columns.
   111  type position struct {
   112  	Line   int `json:"line_number" yaml:"line_number"`
   113  	Column int `json:"column_number" yaml:"column_number"`
   114  }
   115  
   116  // fileLocation describes a location in a source code file.
   117  //
   118  // Note: Positions are one-indexed, as a human counts lines or columns
   119  // in a file.
   120  type fileLocation struct {
   121  	Start position `json:"start_position" yaml:"start_position"`
   122  	End   position `json:"end_position" yaml:"end_position"`
   123  	Path  string   `json:"path" yaml:"path"`
   124  }
   125  
   126  // fileLocationFromPBLocation returns a new fileLocation object based on a
   127  // protocol buffer SourceCodeInfo_Location
   128  func fileLocationFromPBLocation(l *dpb.SourceCodeInfo_Location, d desc.Descriptor) fileLocation {
   129  	// Spans are guaranteed by protobuf to have either three or four ints.
   130  	span := []int32{0, 0, 1}
   131  	if l != nil {
   132  		span = l.Span
   133  	}
   134  
   135  	var fl fileLocation
   136  	if d != nil {
   137  		fl = fileLocation{Path: d.GetFile().GetName()}
   138  	}
   139  
   140  	// If `span` has four ints; they correspond to
   141  	// [start line, start column, end line, end column].
   142  	//
   143  	// We add one because spans are zero-indexed, but not to the end column
   144  	// because we want the ending position to be inclusive and not exclusive.
   145  	if len(span) == 4 {
   146  		fl.Start = position{
   147  			Line:   int(span[0]) + 1,
   148  			Column: int(span[1]) + 1,
   149  		}
   150  		fl.End = position{
   151  			Line:   int(span[2]) + 1,
   152  			Column: int(span[3]),
   153  		}
   154  		return fl
   155  	}
   156  
   157  	// Okay, `span` has three ints; they correspond to
   158  	// [start line, start column, end column].
   159  	//
   160  	// We add one because spans are zero-indexed, but not to the end column
   161  	// because we want the ending position to be inclusive and not exclusive.
   162  	fl.Start = position{
   163  		Line:   int(span[0]) + 1,
   164  		Column: int(span[1]) + 1,
   165  	}
   166  	fl.End = position{
   167  		Line:   int(span[0]) + 1,
   168  		Column: int(span[2]),
   169  	}
   170  	return fl
   171  }