forked from PortAudio/portaudio
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pa_whitelint.py
297 lines (253 loc) · 11.4 KB
/
pa_whitelint.py
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
# PortAudio Repository Whitespace Linter
#
# Run this script from the root of the repository using the command:
# python pa_whitelint.py
#
# Check all source files for the following:
# 1. Consistent line endings are used throughout each file.
# 2. No tabs are present. Use spaces for indenting.
# 3. Indenting: leading whitespace is usually a multiple of 4 spaces,
# with permissive exceptions for continuation lines.
# 4. Lines have no trailing whitespace.
# 5. No non-ASCII or weird control characters are present.
# 6. End-of-line is present at end-of-file.
# 7. No empty (or whitespace) lines at end-of-file.
from pathlib import Path
import re
import sys
# Configuration:
# Check these file types:
sourceFileTypes = ["*.c", "*.h", "*.cpp", "*.cxx", "*.hxx"]
# Scan these directories
dirs = ["src", "include", "examples", "test", "qa"]
# Exclude files or directories with the following names:
excludePathParts = [
"ASIOSDK",
"iasiothiscallresolver.cpp",
"iasiothiscallresolver.h",
"mingw-include",
]
indentSpaceCount = 4
verbose = True
checkBadIndenting = True
verboseBadIndenting = True
# (End configuration)
class FileStatus:
"""Issue status for a particular file. Stores issue counts for each type of issue."""
def __init__(self, path):
self.path = path
issueNames = [
"has-inconsistent-line-endings",
"has-tabs",
"has-bad-indenting",
"has-trailing-whitespace",
"has-bad-character",
"has-empty-line-at-end-of-file",
"has-no-eol-character-at-end-of-file",
]
self.issueCounts = dict.fromkeys(issueNames, 0)
def incrementIssueCount(self, issueName):
assert issueName in self.issueCounts # catch typos in issueName
self.issueCounts[issueName] += 1
def hasIssue(self, issueName):
return self.issueCounts[issueName] > 0
def hasIssues(self):
return any(count > 0 for count in self.issueCounts.values())
def issueSummaryString(self):
return str.join(", ", [name for name in self.issueCounts if self.issueCounts[name] > 0])
def multilineCommentIsOpenAtEol(lineText, wasOpenAtStartOfLine):
isOpen = wasOpenAtStartOfLine
index = 0
end = len(lineText)
while index != -1 and index < end:
if isOpen:
index = lineText.find(b"*/", index)
if index != -1:
isOpen = False
index += 2
else:
index = lineText.find(b"/*", index)
if index != -1:
isOpen = True
index += 2
return isOpen
def allowStrangeIndentOnFollowingLine(lineText):
"""Compute whether a non-standard indent is allowed on the following line.
A line allows an unusual indent to follow if it is the beginning of a
multi-line function parameter list, an element of a function parameter list,
or an incomplete expression (binary operator, etc.).
"""
s = lineText.strip(b" ")
if len(s) == 0:
return False
if s.rfind(b"*/") == (len(s) - 2): # line has a trailing comment, strip it
commentStart = s.rfind(b"/*")
if commentStart != -1:
s = s[:commentStart].strip(b" ")
if len(s) == 0:
return False
if len(s) == 0:
return False
okChars = b'(,\\+-/*=&|?:"'
if s[-1] in okChars: # non-comment program text has trailing okChar: '(' or ',' etc.
return True
return False
def allowStrangeIndentOfLine(lineText):
"""Compute whether a non-standard indent is allowed on the line.
A line is allowed an unusual indent if it is the continuation of an
incomplete expression (binary operator, etc.).
"""
s = lineText.strip(b" ")
if len(s) == 0:
return False
okChars = b'+-/*=&|?:)"'
if s[0] in okChars:
return True
return False
# Run the checks over all files specified by [sourceFileTypes, dirs, excludePathParts]:
statusSummary = []
for dir in dirs:
for ext in sourceFileTypes:
for path in Path(dir).rglob(ext):
if any(part in path.parts for part in excludePathParts):
continue
# during development, uncomment the following 2 lines and select a specific path:
#if not "qa" in path.parts:
# continue
data = path.read_bytes()
status = FileStatus(path)
statusSummary.append(status)
# Perform checks:
# 1. Consistent line endings
# check and then normalize to \n line endings for the benefit of the rest of the program
if b"\r" in data and b"\n" in data:
# CRLF (Windows) case: check for stray CR or LF, then convert CRLF to LF
assert not b"\f" in data # we'll use \f as a sentinel during conversion
d = data.replace(b"\r\n", b"\f")
if b"\r" in d:
status.incrementIssueCount("has-inconsistent-line-endings")
if verbose:
print("error: {0} stray carriage return".format(path))
if b"\n" in d:
status.incrementIssueCount("has-inconsistent-line-endings")
if verbose:
print("error: {0} stray newline".format(path))
data = d.replace(b"\f", b"\n") # normalize line endings
elif b"\r" in data:
# CR (Classic Mac) case: convert CR to LF
data = d.replace(b"\r", b"\n") # normalize line endings
else:
# LF (Unix) case: no change
pass
lines = data.split(b"\n") # relies on newline normalization above
# 2. Absence of tabs
lineNo = 1
for line in lines:
if b"\t" in line:
status.incrementIssueCount("has-tabs")
if verbose:
print("error: {0}({1}) contains tab".format(path, lineNo))
lineNo += 1
data = data.replace(b"\t", b" "*indentSpaceCount) # normalize tabs to <indentSpaceCount> spaces for indent algorithm below
lines = data.split(b"\n") # recompute lines, relies on newline normalization above
# 3. Correct leading whitespace / bad indenting
if checkBadIndenting:
leadingWhitespaceRe = re.compile(b"^\s*")
commentIsOpen = False
previousLine = b""
previousIndent = 0
lineNo = 1
for line in lines:
if commentIsOpen:
# don't check leading whitespace inside comments
commentIsOpen = multilineCommentIsOpenAtEol(line, commentIsOpen)
previousIndent = 0
else:
m = leadingWhitespaceRe.search(line)
indent = m.end() - m.start()
if indent != len(line): # ignore whitespace lines, they are considered trailing whitespace
if indent % indentSpaceCount != 0 and indent != previousIndent:
# potential bad indents are not multiples of <indentSpaceCount>,
# and are not indented the same as the previous line
s = previousLine
if not allowStrangeIndentOnFollowingLine(previousLine) and not allowStrangeIndentOfLine(line):
status.incrementIssueCount("has-bad-indenting")
if verbose or verboseBadIndenting:
print("error: {0}({1}) bad indent: {2}".format(path, lineNo, indent))
print(line)
commentIsOpen = multilineCommentIsOpenAtEol(line, commentIsOpen)
previousIndent = indent
previousLine = line
lineNo += 1
# 4. No trailing whitespace
trailingWhitespaceRe = re.compile(b"\s*$")
lineNo = 1
for line in lines:
m = trailingWhitespaceRe.search(line)
trailing = m.end() - m.start()
if trailing > 0:
status.incrementIssueCount("has-trailing-whitespace")
if verbose:
print("error: {0}({1}) trailing whitespace:".format(path, lineNo))
print(line)
lineNo += 1
# 5. No non-ASCII or weird control characters
badCharactersRe = re.compile(b"[^\t\r\n\x20-\x7E]+")
lineNo = 1
for line in lines:
m = badCharactersRe.search(line)
if m:
bad = m.end() - m.start()
if bad > 0:
status.incrementIssueCount("has-bad-character")
if verbose:
print("error: {0}({1}) bad character:".format(path, lineNo))
print(line)
lineNo += 1
# 6. Require EOL at EOF
if len(data) == 0:
status.incrementIssueCount("has-no-eol-character-at-end-of-file")
if verbose:
lineNo = 1
print("error: {0}({1}) no end-of-line at end-of-file (empty file)".format(path, lineNo))
else:
lastChar = data[-1]
if lastChar != b"\n"[0]:
status.incrementIssueCount("has-no-eol-character-at-end-of-file")
if verbose:
lineNo = len(lines)
print("error: {0}({1}) no end-of-line at end-of-file".format(path, lineNo))
# 7. No "empty" (or whitespace) lines at end-of-file.
# Cases:
# 1. There is an EOL at EOF. Since the lines array is constructed by splitting on '\n',
# the final element in the lines array will be an empty string. This is expeced and allowed.
# Then continue to check for earlier empty lines.
# 2. There is no EOF at EOL.
# Check for empty lines, including the final line.
expectEmptyFinalLine = not status.hasIssue("has-no-eol-character-at-end-of-file") # i.e. we have EOL at EOF
finalLineNo = len(lines)
lineNo = finalLineNo
for line in reversed(lines):
if lineNo == finalLineNo and expectEmptyFinalLine:
assert len(line) == 0 # this is guaranteed, since lines = data.split('\n') and there is an EOL at EOF
else:
s = line.strip(b" ") # whitespace-only-lines count as empty
if len(s) == 0:
status.incrementIssueCount("has-empty-line-at-end-of-file")
if verbose:
print("error: {0}({1}) empty line at end-of-file".format(path, lineNo))
else:
break # stop checking once we encounter a non-empty line
lineNo -= 1
print("SUMMARY")
print("=======")
issuesFound = False
for s in statusSummary:
if s.hasIssues():
issuesFound = True
print("error: " + str(s.path) + " (" + s.issueSummaryString() + ")")
if issuesFound:
sys.exit(1)
else:
print("all good.")
sys.exit(0)