forked from Azure/azure-sdk-tools
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCodeownersLinter.cs
More file actions
325 lines (310 loc) · 17.7 KB
/
CodeownersLinter.cs
File metadata and controls
325 lines (310 loc) · 17.7 KB
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Azure.Sdk.Tools.CodeownersUtils.Constants;
using Azure.Sdk.Tools.CodeownersUtils.Errors;
using Azure.Sdk.Tools.CodeownersUtils.Utils;
namespace Azure.Sdk.Tools.CodeownersUtils.Verification
{
/// <summary>
/// The primary entry point for Linting a CODEOWNERS file
/// </summary>
public static class CodeownersLinter
{
/// <summary>
/// Load the Codeowners file and process it a block at a time
/// </summary>
/// <param name="directoryUtils">Directory utils used for source path entry verification.</param>
/// <param name="ownerData">Owner data used for owner verification.</param>
/// <param name="repoLabelData">Repository label data used for label verification.</param>
/// <param name="codeownersFileFullPath">Codeowners file with full path</param>
/// <returns></returns>
public static List<BaseError> LintCodeownersFile(DirectoryUtils directoryUtils,
OwnerDataUtils ownerData,
RepoLabelDataUtils repoLabelData,
string codeownersFileFullPath)
{
List<BaseError> errors = new List<BaseError>();
// Load the codeowners file and process it a block at a time
List<string> codeownersFile = FileHelpers.LoadFileAsStringList(codeownersFileFullPath);
// Start parsing the codeowners file, a block at a time.
// A block can be one of the following:
// 1. A single source path/owner line
// 2. One or more monikers that either ends in source path/owner line or a blank line, depending
// on the moniker.
for (int currentLineNum = 0; currentLineNum < codeownersFile.Count; currentLineNum++)
{
string line = codeownersFile[currentLineNum];
if (ParsingUtils.IsMonikerOrSourceLine(line))
{
// A block can be a single line, if it's a source path/owners line, or if the block starts
// with a moniker, it'll be multi-line
int blockEnd = ParsingUtils.FindBlockEnd(currentLineNum, codeownersFile);
VerifyBlock(directoryUtils,
ownerData,
repoLabelData,
errors,
currentLineNum,
blockEnd,
codeownersFile);
// After processing the block, set the current line to the end line which will get
// incremented and continue the processing the line after the block
currentLineNum = blockEnd;
}
}
return errors;
}
/// <summary>
/// Basically a call to VerifyBlock that'll skip single line verification. This is used by the parser
/// which only needs to know if there are block errors. Because there's no single line verification
/// the DirectoryUtils, OwnerDataUtils and RepoLabelDataUtils are unnecessary and can be null.
/// </summary>
/// <param name="errors">List of errors that will be appended to if any are found with the block</param>
/// <param name="startBlockLineNumber">The line number which is the start of the block to verify</param>
/// <param name="endBlockLineNumber">Int, the line number that marks the of the block</param>
/// <param name="codeownersFile">The List<string> that represents the CODEOWNERS file</param>
public static void VerifyBlock(List<BaseError> errors,
int startBlockLineNumber,
int endBlockLineNumber,
List<string> codeownersFile)
{
VerifyBlock(null,
null,
null,
errors,
startBlockLineNumber,
endBlockLineNumber,
codeownersFile,
false /* don't do single line verification */ );
}
/// <summary>
/// Verify the formatting of a block in codeowners.
/// Definitions:
/// Source path/Owner Line: Any line in CODEOWNERS that is not a comment and not blank.
/// Metadata block : A metadata block is a block that consists of one or more metadata tags which, depending on the tags,
/// may end with a source path/owner line.
/// </summary>
/// <param name="directoryUtils">Owner data used for owner verification.</param>
/// <param name="ownerData">Owner data used for owner verification.</param>
/// <param name="repoLabelData">Repository label data used for label verification.</param>
/// <param name="errors">List of errors that will be appended to if any are found with the block</param>
/// <param name="startBlockLineNumber">The line number which is the start of the block to verify</param>
/// <param name="endBlockLineNumber">Int, the line number that marks the of the block</param>
/// <param name="codeownersFile">The List<string> that represents the CODEOWNERS file</param>
/// <param name="singleLineVerification">Whether or not to perform single line verification, default is true. The
/// reason this would be turned off would be parsing, which just needs to verify the block is good.</param>
public static void VerifyBlock(DirectoryUtils directoryUtils,
OwnerDataUtils ownerData,
RepoLabelDataUtils repoLabelData,
List<BaseError> errors,
int startBlockLineNumber,
int endBlockLineNumber,
List<string> codeownersFile,
bool singleLineVerification = true)
{
List<string> blockErrorStrings = new List<string>();
// The codeownersFile as a list<string> is 0 based, for reporting purposes it needs
// to be 1 based to match the exact line in the CODEOWNERS file.
int startLineNumberForReporting = startBlockLineNumber + 1;
int endLineNumberForReporting = endBlockLineNumber + 1;
bool endsWithSourceOwnerLine = ParsingUtils.IsSourcePathOwnerLine(codeownersFile[endBlockLineNumber]);
// Booleans for every moniker, will be set to true when found, are used to verify the block
// contains what it needs to contain for the monikers found within it.
bool blockHasAzureSdkOwners = false;
bool blockHasMissingFolder = false;
bool blockHasPRLabel = false;
bool blockHasServiceLabel = false;
bool blockHasServiceOwners = false;
for (int blockLine = startBlockLineNumber; blockLine <= endBlockLineNumber; blockLine++)
{
string line = codeownersFile[blockLine];
int lineNumberForReporting = blockLine + 1;
bool isSourcePathOwnerLine = ParsingUtils.IsSourcePathOwnerLine(line);
if (isSourcePathOwnerLine)
{
if (singleLineVerification)
{
VerifySingleLine(directoryUtils,
ownerData,
repoLabelData,
errors,
lineNumberForReporting,
line,
isSourcePathOwnerLine,
!endsWithSourceOwnerLine);
}
}
else
{
string moniker = MonikerUtils.ParseMonikerFromLine(line);
// This can happen if there's a comment line in the block, skip the line
if (null == moniker)
{
continue;
}
switch (moniker)
{
case MonikerConstants.AzureSdkOwners:
if (blockHasAzureSdkOwners)
{
blockErrorStrings.Add($"{MonikerConstants.AzureSdkOwners}{ErrorMessageConstants.DuplicateMonikerInBlockPartial}");
}
blockHasAzureSdkOwners = true;
break;
case MonikerConstants.PRLabel:
if (blockHasPRLabel)
{
blockErrorStrings.Add($"{MonikerConstants.PRLabel}{ErrorMessageConstants.DuplicateMonikerInBlockPartial}");
}
blockHasPRLabel = true;
break;
case MonikerConstants.MissingFolder:
if (blockHasMissingFolder)
{
blockErrorStrings.Add($"{MonikerConstants.MissingFolder}{ErrorMessageConstants.DuplicateMonikerInBlockPartial}");
}
blockHasMissingFolder = true;
break;
case MonikerConstants.ServiceLabel:
if (blockHasServiceLabel)
{
blockErrorStrings.Add($"{MonikerConstants.ServiceLabel}{ErrorMessageConstants.DuplicateMonikerInBlockPartial}");
}
blockHasServiceLabel = true;
break;
case MonikerConstants.ServiceOwners:
if (blockHasServiceOwners)
{
blockErrorStrings.Add($"{MonikerConstants.ServiceOwners}{ErrorMessageConstants.DuplicateMonikerInBlockPartial}");
}
blockHasServiceOwners = true;
break;
default:
// This shouldn't get here unless someone adds a new moniker and forgets to add it to the switch statement
throw new ArgumentException($"Unexpected moniker '{moniker}' found on line {lineNumberForReporting}\nLine={line}");
}
if (singleLineVerification)
{
VerifySingleLine(directoryUtils,
ownerData,
repoLabelData,
errors,
lineNumberForReporting,
line,
isSourcePathOwnerLine,
!endsWithSourceOwnerLine, // If the block ends in a source path/owner line then we don't expect owners on moniker lines
moniker);
}
}
}
// After the block has been processed, ensure that any monikers are paired correctly with other
// monikers or source path/owners
// If the block is a single source path/owners line then there's nothing else to be done since there
// can't be any block errors.
if (startBlockLineNumber == endBlockLineNumber && endsWithSourceOwnerLine)
{
return;
}
// AzureSdkOwners must be part of a block of that a ServiceLabel entry as the AzureSdkOwners are associated with
// that ServiceLabel
if (blockHasAzureSdkOwners && !blockHasServiceLabel)
{
blockErrorStrings.Add(ErrorMessageConstants.AzureSdkOwnersMustBeWithServiceLabel);
}
if (blockHasServiceOwners && !blockHasServiceLabel)
{
blockErrorStrings.Add(ErrorMessageConstants.ServiceOwnersMustBeWithServiceLabel);
}
// PRLabel moniker must be in a block that ends with a source path/owner line
if (blockHasPRLabel && !endsWithSourceOwnerLine)
{
blockErrorStrings.Add($"{MonikerConstants.PRLabel}{ErrorMessageConstants.NeedsToEndWithSourceOwnerPartial}");
}
// ServiceLabel needs to be part of a block that has AzureSdkOwners or one of; ServiceOwners or #/<NotInRepo>/
// (MonikerConstants.MissingFolder), or ends in a source path/owner line both not both.
if (blockHasServiceLabel)
{
if (!endsWithSourceOwnerLine && !blockHasServiceOwners && !blockHasMissingFolder && !blockHasAzureSdkOwners)
{
blockErrorStrings.Add(ErrorMessageConstants.ServiceLabelNeedsOwners);
}
else if (endsWithSourceOwnerLine && (blockHasServiceOwners || blockHasMissingFolder))
{
blockErrorStrings.Add(ErrorMessageConstants.ServiceLabelHasTooManyOwners);
}
else if (blockHasServiceOwners && blockHasMissingFolder)
{
blockErrorStrings.Add(ErrorMessageConstants.ServiceLabelHasTooManyOwnerMonikers);
}
}
if (blockErrorStrings.Count > 0)
{
List<string> blockLines = new List<string>();
blockLines.AddRange(codeownersFile.GetRange(startBlockLineNumber, (endBlockLineNumber - startBlockLineNumber) + 1));
errors.Add(new BlockFormattingError(startLineNumberForReporting,
endLineNumberForReporting,
blockLines,
blockErrorStrings));
}
}
/// <summary>
/// Verify the contents of a single line, called as part of the block processing.
/// </summary>
/// <param name="ownerData">Owner data used for owner verification.</param>
/// <param name="repoLabelData">Repository label data used for label verification.</param>
/// <param name="errors">List of errors that will be appended to if any are found with the block</param>
/// <param name="lineNumberForReporting">The line number, for reporting purposes, of the line being processed.</param>
/// <param name="line">The CODEOWNERS line to process.</param>
/// <param name="expectOwnersIfMoniker">True if owners are expected on a moniker line. This would be true if the moniker is part of a block that didn't end in a source path/owner line.</param>
public static void VerifySingleLine(DirectoryUtils directoryUtils,
OwnerDataUtils ownerData,
RepoLabelDataUtils repoLabelData,
List<BaseError> errors,
int lineNumberForReporting,
string line,
bool isSourcePathOwnerLine,
bool expectOwnersIfMoniker,
string moniker = null)
{
List<string> errorStrings = new List<string>();
if (isSourcePathOwnerLine)
{
// Verify the source path and owners
directoryUtils.VerifySourcePathEntry(line, errorStrings);
Owners.VerifyOwners(ownerData, line, isSourcePathOwnerLine, true, errorStrings);
}
else
{
// At this point, the moniker shouldn't be null, comment lines should have been
// sifted out by the calling method.
if (null != moniker)
{
switch (moniker)
{
// Both ServiceLabel and blockHasPRLabel moniker lines need to have labels
case MonikerConstants.ServiceLabel:
case MonikerConstants.PRLabel:
Labels.VerifyLabels(repoLabelData, line, moniker, errorStrings);
break;
// MissingFolder, ServiceOwners and AzureSdkOwners
case MonikerConstants.MissingFolder:
case MonikerConstants.ServiceOwners:
case MonikerConstants.AzureSdkOwners:
Owners.VerifyOwners(ownerData, line, isSourcePathOwnerLine, expectOwnersIfMoniker, errorStrings);
break;
default:
// This shouldn't get here unless someone adds a new moniker and forgets to add it to the switch statement
throw new ArgumentException($"Unexpected moniker '{moniker}' found on line {lineNumberForReporting}\nLine={line}");
}
}
}
// If any errors were encountered on the line, create a new exception and add it to the list
if (errorStrings.Count > 0)
{
errors.Add(new SingleLineError(lineNumberForReporting, line, errorStrings));
}
}
}
}