Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
eclipse
GitHub Repository: eclipse/sumo
Path: blob/main/tools/build_config/checkStyle.py
169673 views
1
#!/usr/bin/env python
2
# Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
3
# Copyright (C) 2010-2025 German Aerospace Center (DLR) and others.
4
# This program and the accompanying materials are made available under the
5
# terms of the Eclipse Public License 2.0 which is available at
6
# https://www.eclipse.org/legal/epl-2.0/
7
# This Source Code may also be made available under the following Secondary
8
# Licenses when the conditions for such availability set forth in the Eclipse
9
# Public License 2.0 are satisfied: GNU General Public License, version 2
10
# or later which is available at
11
# https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
12
# SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
13
14
# @file checkStyle.py
15
# @author Michael Behrisch
16
# @date 2010-08-29
17
18
"""
19
Checks svn property settings for all files and pep8 for python
20
as well as file headers.
21
"""
22
from __future__ import absolute_import
23
from __future__ import print_function
24
25
import os
26
import sys
27
import subprocess
28
import multiprocessing
29
import xml.sax
30
import codecs
31
from optparse import OptionParser
32
try:
33
import flake8 # noqa
34
HAVE_FLAKE = True
35
except ImportError:
36
print("Python flake not found. Python style checking is disabled.")
37
HAVE_FLAKE = False
38
try:
39
import autopep8 # noqa
40
HAVE_AUTOPEP = True
41
except ImportError:
42
HAVE_AUTOPEP = False
43
try:
44
with open(os.devnull, 'w') as devnull:
45
subprocess.check_call(['astyle', '--version'], stdout=devnull)
46
HAVE_ASTYLE = True
47
except (OSError, subprocess.CalledProcessError):
48
HAVE_ASTYLE = False
49
50
_SOURCE_EXT = set([".h", ".cpp", ".py", ".pyw", ".pl", ".java", ".am", ".cs", ".c"])
51
_TESTDATA_EXT = [".xml", ".prog", ".csv",
52
".complex", ".dfrouter", ".duarouter", ".jtrrouter", ".marouter",
53
".astar", ".chrouter", ".internal", ".tcl", ".txt",
54
".netconvert", ".netedit", ".netgen",
55
".od2trips", ".polyconvert", ".sumo",
56
".meso", ".tools", ".traci", ".activitygen",
57
".scenario", ".tapasVEU",
58
".sumocfg", ".netccfg", ".netgcfg"]
59
_VS_EXT = [".vsprops", ".sln", ".vcproj",
60
".bat", ".props", ".vcxproj", ".filters"]
61
_IGNORE = set(["binstate.sumo", "binstate.sumo.meso", "image.tools"])
62
_KEYWORDS = "HeadURL Id LastChangedBy LastChangedDate LastChangedRevision"
63
64
SEPARATOR = "/****************************************************************************/\n"
65
EPL_HEADER = """/****************************************************************************/
66
// Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
67
// Copyright (C) 2001-2019 German Aerospace Center (DLR) and others.
68
// This program and the accompanying materials
69
// are made available under the terms of the Eclipse Public License v2.0
70
// which accompanies this distribution, and is available at
71
// http://www.eclipse.org/legal/epl-v20.html
72
// SPDX-License-Identifier: EPL-2.0
73
/****************************************************************************/
74
"""
75
EPL_GPL_HEADER = """/****************************************************************************/
76
// Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
77
// Copyright (C) 2001-2025 German Aerospace Center (DLR) and others.
78
// This program and the accompanying materials are made available under the
79
// terms of the Eclipse Public License 2.0 which is available at
80
// https://www.eclipse.org/legal/epl-2.0/
81
// This Source Code may also be made available under the following Secondary
82
// Licenses when the conditions for such availability set forth in the Eclipse
83
// Public License 2.0 are satisfied: GNU General Public License, version 2
84
// or later which is available at
85
// https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
86
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
87
/****************************************************************************/
88
"""
89
90
91
class PropertyReader(xml.sax.handler.ContentHandler):
92
93
"""Reads the svn properties of files as written by svn pl -v --xml"""
94
95
def __init__(self, doFix, doPep, license_attrs):
96
self._fix = doFix
97
self._pep = doPep
98
self._license = dict([e.split(":") for e in license_attrs.split(",")]) if license_attrs else None
99
self._file = ""
100
self._property = None
101
self._value = ""
102
self._hadEOL = False
103
self._hadKeywords = False
104
self._haveFixed = False
105
106
def checkDoxyLines(self, lines, idx, comment="///"):
107
fileRef = "%s @file %s\n" % (comment, os.path.basename(self._file))
108
try:
109
s = lines[idx].split()
110
if s != fileRef.split():
111
print(self._file, "broken @file reference", lines[idx].rstrip())
112
if self._fix and lines[idx].startswith("%s @file" % comment):
113
lines[idx] = fileRef
114
self._haveFixed = True
115
idx += 1
116
s = lines[idx].split()
117
if s[:2] != [comment, "@author"]:
118
print(self._file, "broken @author reference", s)
119
idx += 1
120
while lines[idx].split()[:2] == [comment, "@author"]:
121
idx += 1
122
s = lines[idx].split()
123
if s[:2] != [comment, "@date"]:
124
print(self._file, "broken @date reference", s)
125
idx += 1
126
if lines[idx] not in (comment + "\n", "\n"):
127
print(self._file, "missing empty line", idx, lines[idx].rstrip())
128
except IndexError:
129
print(self._file, "seems to be empty")
130
idx += 1
131
while idx < len(lines) and lines[idx].split()[:1] == ["//"]:
132
idx += 1
133
return idx
134
135
def checkFileHeader(self, ext):
136
with open(self._file) as f:
137
lines = f.readlines()
138
if len(lines) == 0:
139
print(self._file, "is empty")
140
return
141
self._haveFixed = False
142
idx = 0
143
if ext in (".cpp", ".h", ".java", ".c"):
144
if lines[idx] == SEPARATOR:
145
year = lines[idx + 2][17:21]
146
end = lines.index(SEPARATOR, idx + 1) + 1
147
license = EPL_HEADER.replace("2001", year)
148
newLicense = EPL_GPL_HEADER.replace("2001", year)
149
if "module" in lines[idx + 3]:
150
fileLicense = "".join(lines[idx:idx + 3]) + "".join(lines[idx + 5:end])
151
else:
152
fileLicense = "".join(lines[idx:end])
153
if fileLicense == license:
154
print(self._file, "old license")
155
if self._license:
156
if "module" in lines[idx + 3]:
157
lines[idx:idx+3] = newLicense.splitlines(True)[:3]
158
lines[idx+5:end] = newLicense.splitlines(True)[3:]
159
else:
160
lines[idx:end] = newLicense.splitlines(True)
161
end += 4
162
self._haveFixed = True
163
elif fileLicense != newLicense:
164
print(self._file, "invalid license")
165
if options.verbose:
166
print(fileLicense)
167
print(license)
168
self.checkDoxyLines(lines, end)
169
else:
170
print(self._file, "header does not start")
171
if ext in (".py", ".pyw"):
172
if lines[0][:2] == '#!':
173
idx += 1
174
if lines[0] not in ('#!/usr/bin/env python\n', '#!/usr/bin/env python3\n', '#!/usr/bin/env python2\n'):
175
print(self._file, "wrong shebang")
176
if self._fix:
177
lines[0] = '#!/usr/bin/env python\n'
178
self._haveFixed = True
179
if lines[idx][:5] == '# -*-':
180
idx += 1
181
end = lines.index("\n", idx) if "\n" in lines else idx
182
if len(lines) < 13:
183
print(self._file, "is too short (%s lines, at least 13 required for valid header)" % len(lines))
184
if not self._license:
185
return
186
year = lines[idx + 1][16:20] if len(lines) > idx + 1 else ""
187
if self._license:
188
year = self._license.get("year", "2001")
189
license = EPL_HEADER.replace("// ", "# ").replace("// ", "# ").replace("\n//", "")
190
license = license.replace("2001", year).replace(SEPARATOR, "")
191
newLicense = EPL_GPL_HEADER.replace("// ", "# ").replace("// ", "# ").replace("\n//", "")
192
newLicense = newLicense.replace("2001", year).replace(SEPARATOR, "")
193
if len(lines) > idx + 4 and "module" in lines[idx + 2]:
194
fileLicense = "".join(lines[idx:idx + 2]) + "".join(lines[idx + 4:end])
195
else:
196
fileLicense = "".join(lines[idx:end])
197
if fileLicense == license:
198
print(self._file, "old license")
199
if self._license:
200
if "module" in lines[idx + 2]:
201
lines[idx:idx+2] = newLicense.splitlines(True)[:2]
202
lines[idx+4:end] = newLicense.splitlines(True)[2:]
203
else:
204
lines[idx:end] = newLicense.splitlines(True)
205
end += 4
206
self._haveFixed = True
207
elif fileLicense != newLicense:
208
if self._license:
209
lines[idx:idx] = newLicense.splitlines(True) + ["\n",
210
"# @file %s\n" % os.path.basename(self._file),
211
"# @author %s\n" % self._license.get("author", ""),
212
"# @date %s\n" % self._license.get("date", ""),
213
"\n"]
214
if "module" in self._license:
215
lines[idx+2:idx+2] = ["# %s\n" % ml for ml in self._license["module"].split('\\n')]
216
self._haveFixed = True
217
else:
218
print(self._file, "different license:")
219
print(fileLicense)
220
if options.verbose:
221
print("!!%s!!" % os.path.commonprefix([fileLicense, license]))
222
print(license)
223
self.checkDoxyLines(lines, end + 1, "#")
224
if self._haveFixed:
225
open(self._file, "w").write("".join(lines))
226
227
def startElement(self, name, attrs):
228
if name == 'target':
229
self._file = attrs['path']
230
seen.add(os.path.join(repoRoot, self._file))
231
if name == 'property':
232
self._property = attrs['name']
233
234
def characters(self, content):
235
if self._property:
236
self._value += content
237
238
def endElement(self, name):
239
ext = os.path.splitext(self._file)[1]
240
if name == 'property' and self._property == "svn:eol-style":
241
self._hadEOL = True
242
if name == 'property' and self._property == "svn:keywords":
243
self._hadKeywords = True
244
if os.path.basename(self._file) not in _IGNORE:
245
if ext in _SOURCE_EXT or ext in _TESTDATA_EXT or ext in _VS_EXT:
246
if (name == 'property' and self._property == "svn:executable" and
247
ext not in (".py", ".pyw", ".pl", ".bat")):
248
print(self._file, self._property, self._value)
249
if self._fix:
250
subprocess.call(
251
["svn", "pd", "svn:executable", self._file])
252
if name == 'property' and self._property == "svn:mime-type":
253
print(self._file, self._property, self._value)
254
if self._fix:
255
subprocess.call(
256
["svn", "pd", "svn:mime-type", self._file])
257
if ext in _SOURCE_EXT or ext in _TESTDATA_EXT:
258
if ((name == 'property' and self._property == "svn:eol-style" and self._value != "LF") or
259
(name == "target" and not self._hadEOL)):
260
print(self._file, "svn:eol-style", self._value)
261
if self._fix:
262
if os.name == "posix":
263
subprocess.call(
264
["sed", "-i", r's/\r$//', self._file])
265
subprocess.call(
266
["sed", "-i", r's/\r/\n/g', self._file])
267
subprocess.call(
268
["svn", "ps", "svn:eol-style", "LF", self._file])
269
if ext in _SOURCE_EXT:
270
if ((name == 'property' and self._property == "svn:keywords" and self._value != _KEYWORDS) or
271
(name == "target" and not self._hadKeywords)):
272
print(self._file, "svn:keywords", self._value)
273
if self._fix:
274
subprocess.call(
275
["svn", "ps", "svn:keywords", _KEYWORDS, self._file])
276
if name == 'target':
277
p = self.checkFile()
278
if p is not None:
279
p.wait()
280
if ext in _VS_EXT:
281
if ((name == 'property' and self._property == "svn:eol-style" and self._value != "CRLF") or
282
(name == "target" and not self._hadEOL)):
283
print(self._file, "svn:eol-style", self._value)
284
if self._fix:
285
subprocess.call(["svn", "ps", "svn:eol-style", "CRLF", self._file])
286
if name == 'property':
287
self._value = ""
288
self._property = None
289
if name == 'target':
290
self._hadEOL = False
291
self._hadKeywords = False
292
293
def checkFile(self, fileName=None, exclude=None):
294
if fileName is not None:
295
self._file = fileName
296
ext = os.path.splitext(self._file)[1]
297
try:
298
with codecs.open(self._file, 'r', 'utf8') as f:
299
f.read()
300
except UnicodeDecodeError as err:
301
print(self._file, err)
302
self.checkFileHeader(ext)
303
if exclude:
304
for x in exclude:
305
if x + "/" in self._file:
306
return None
307
if ext in (".cpp", ".h", ".java") and HAVE_ASTYLE and self._fix:
308
subprocess.call(["astyle", "--style=java", "--unpad-paren", "--pad-header", "--pad-oper",
309
"--add-brackets", "--indent-switches", "--align-pointer=type",
310
"-n", os.path.abspath(self._file)])
311
subprocess.call(["sed", "-i", "-e", '$a\\', self._file])
312
if self._pep and ext == ".py":
313
ret = 0
314
if HAVE_FLAKE and os.path.getsize(self._file) < 1000000: # flake hangs on very large files
315
flake = "flake8-2" if sys.version_info[0] < 3 else "flake8"
316
if not self._fix:
317
return subprocess.Popen([flake, "--max-line-length", "120", self._file])
318
ret = subprocess.call([flake, "--max-line-length", "120", self._file])
319
if ret and HAVE_AUTOPEP and self._fix:
320
subprocess.call(["autopep8", "--max-line-length", "120", "--in-place", self._file])
321
322
323
optParser = OptionParser()
324
optParser.add_option("-v", "--verbose", action="store_true",
325
default=False, help="tell me what you are doing")
326
optParser.add_option("-f", "--fix", action="store_true",
327
default=False, help="fix invalid svn properties, run astyle and autopep8")
328
optParser.add_option("-l", "--license",
329
help="fix / insert license (needs at least one of the attributes module, author, year, date)")
330
optParser.add_option("-s", "--skip-pep", action="store_true",
331
default=False, help="skip autopep8 and flake8 tests")
332
optParser.add_option("-d", "--directory", help="check given subdirectory of sumo tree")
333
optParser.add_option("-x", "--exclude", default="contributed,foreign",
334
help="comma-separated list of (sub-)paths to exclude from pep and astyle checks")
335
(options, args) = optParser.parse_args()
336
seen = set()
337
sumoRoot = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
338
if len(args) > 0:
339
repoRoots = [os.path.abspath(a) for a in args]
340
elif options.directory:
341
repoRoots = [os.path.join(sumoRoot, options.directory)]
342
else:
343
repoRoots = [sumoRoot]
344
procs = []
345
for repoRoot in repoRoots:
346
if options.verbose:
347
print("checking", repoRoot)
348
propRead = PropertyReader(options.fix, not options.skip_pep, options.license)
349
if os.path.isfile(repoRoot):
350
proc = propRead.checkFile(repoRoot)
351
if proc is not None:
352
procs.append(proc)
353
continue
354
try:
355
oldDir = os.getcwd()
356
os.chdir(repoRoot)
357
exclude = options.exclude.split(",")
358
for name in subprocess.check_output(["git", "ls-files"], universal_newlines=True).splitlines():
359
ext = os.path.splitext(name)[1]
360
if ext in _SOURCE_EXT:
361
proc = propRead.checkFile(name, exclude)
362
if proc is not None:
363
procs.append(proc)
364
if len(procs) == multiprocessing.cpu_count():
365
[p.wait() for p in procs]
366
procs = []
367
os.chdir(oldDir)
368
continue
369
except (OSError, subprocess.CalledProcessError) as e:
370
print("This seems to be no valid git repository", repoRoot, e)
371
if options.verbose:
372
print("trying svn at", repoRoot)
373
output = subprocess.check_output(["svn", "pl", "-v", "-R", "--xml", repoRoot])
374
xml.sax.parseString(output, propRead)
375
if options.verbose:
376
print("re-checking tree at", repoRoot)
377
for root, dirs, files in os.walk(repoRoot):
378
for name in files:
379
ext = os.path.splitext(name)[1]
380
if name not in _IGNORE:
381
fullName = os.path.join(root, name)
382
if ext in _SOURCE_EXT or ext in _TESTDATA_EXT or ext in _VS_EXT:
383
if fullName in seen or subprocess.call(["svn", "ls", fullName],
384
stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT):
385
continue
386
print(fullName, "svn:eol-style")
387
if options.fix:
388
if ext in _VS_EXT:
389
subprocess.call(
390
["svn", "ps", "svn:eol-style", "CRLF", fullName])
391
else:
392
if os.name == "posix":
393
subprocess.call(["sed", "-i", 's/\r$//', fullName])
394
subprocess.call(
395
["svn", "ps", "svn:eol-style", "LF", fullName])
396
if ext in _SOURCE_EXT:
397
print(fullName, "svn:keywords")
398
if options.fix:
399
subprocess.call(
400
["svn", "ps", "svn:keywords", _KEYWORDS, fullName])
401
for ignoreDir in ['.svn', 'foreign', 'contributed', 'texttesttmp']:
402
if ignoreDir in dirs:
403
dirs.remove(ignoreDir)
404
[p.wait() for p in procs]
405
406