about summary refs log tree commit diff
path: root/third_party/go/git-appraise/review/analyses/analyses.go
blob: 4828f3b230c2242978222387c2e84f4bf2e9d068 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
/*
Copyright 2015 Google Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// Package analyses defines the internal representation of static analysis reports.
package analyses

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"sort"
	"strconv"

	"github.com/google/git-appraise/repository"
)

const (
	// Ref defines the git-notes ref that we expect to contain analysis reports.
	Ref = "refs/notes/devtools/analyses"

	// StatusLooksGoodToMe is the status string representing that analyses reported no messages.
	StatusLooksGoodToMe = "lgtm"
	// StatusForYourInformation is the status string representing that analyses reported informational messages.
	StatusForYourInformation = "fyi"
	// StatusNeedsMoreWork is the status string representing that analyses reported error messages.
	StatusNeedsMoreWork = "nmw"

	// FormatVersion defines the latest version of the request format supported by the tool.
	FormatVersion = 0
)

// Report represents a build/test status report generated by analyses tool.
// Every field is optional.
type Report struct {
	Timestamp string `json:"timestamp,omitempty"`
	URL       string `json:"url,omitempty"`
	Status    string `json:"status,omitempty"`
	// Version represents the version of the metadata format.
	Version int `json:"v,omitempty"`
}

// LocationRange represents the location within a source file that an analysis message covers.
type LocationRange struct {
	StartLine   uint32 `json:"start_line,omitempty"`
	StartColumn uint32 `json:"start_column,omitempty"`
	EndLine     uint32 `json:"end_line,omitempty"`
	EndColumn   uint32 `json:"end_column,omitempty"`
}

// Location represents the location within a source tree that an analysis message covers.
type Location struct {
	Path  string         `json:"path,omitempty"`
	Range *LocationRange `json:"range,omitempty"`
}

// Note represents a single analysis message.
type Note struct {
	Location    *Location `json:"location,omitempty"`
	Category    string    `json:"category,omitempty"`
	Description string    `json:"description"`
}

// AnalyzeResponse represents the response from a static-analysis tool.
type AnalyzeResponse struct {
	Notes []Note `json:"note,omitempty"`
}

// ReportDetails represents an entire static analysis run (which might include multiple analysis tools).
type ReportDetails struct {
	AnalyzeResponse []AnalyzeResponse `json:"analyze_response,omitempty"`
}

// GetLintReportResult downloads the details of a lint report and returns the responses embedded in it.
func (analysesReport Report) GetLintReportResult() ([]AnalyzeResponse, error) {
	if analysesReport.URL == "" {
		return nil, nil
	}
	res, err := http.Get(analysesReport.URL)
	if err != nil {
		return nil, err
	}
	analysesResults, err := ioutil.ReadAll(res.Body)
	res.Body.Close()
	if err != nil {
		return nil, err
	}
	var details ReportDetails
	err = json.Unmarshal([]byte(analysesResults), &details)
	if err != nil {
		return nil, err
	}
	return details.AnalyzeResponse, nil
}

// GetNotes downloads the details of an analyses report and returns the notes embedded in it.
func (analysesReport Report) GetNotes() ([]Note, error) {
	reportResults, err := analysesReport.GetLintReportResult()
	if err != nil {
		return nil, err
	}
	var reportNotes []Note
	for _, reportResult := range reportResults {
		reportNotes = append(reportNotes, reportResult.Notes...)
	}
	return reportNotes, nil
}

// Parse parses an analysis report from a git note.
func Parse(note repository.Note) (Report, error) {
	bytes := []byte(note)
	var report Report
	err := json.Unmarshal(bytes, &report)
	return report, err
}

// GetLatestAnalysesReport takes a collection of analysis reports, and returns the one with the most recent timestamp.
func GetLatestAnalysesReport(reports []Report) (*Report, error) {
	timestampReportMap := make(map[int]*Report)
	var timestamps []int

	for _, report := range reports {
		timestamp, err := strconv.Atoi(report.Timestamp)
		if err != nil {
			return nil, err
		}
		timestamps = append(timestamps, timestamp)
		timestampReportMap[timestamp] = &report
	}
	if len(timestamps) == 0 {
		return nil, nil
	}
	sort.Sort(sort.Reverse(sort.IntSlice(timestamps)))
	return timestampReportMap[timestamps[0]], nil
}

// ParseAllValid takes collection of git notes and tries to parse a analyses report
// from each one. Any notes that are not valid analyses reports get ignored.
func ParseAllValid(notes []repository.Note) []Report {
	var reports []Report
	for _, note := range notes {
		report, err := Parse(note)
		if err == nil && report.Version == FormatVersion {
			reports = append(reports, report)
		}
	}
	return reports
}