eric6/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py

branch
maintenance
changeset 6989
8b8cadf8d7e9
parent 6646
51eefa621de4
parent 6942
2602857055c5
child 7286
7eb04391adf7
equal deleted inserted replaced
6938:7926553b7509 6989:8b8cadf8d7e9
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a simple Python syntax checker.
8 """
9
10 from __future__ import unicode_literals
11
12 import os
13 import fnmatch
14
15 from PyQt5.QtCore import pyqtSlot, Qt, QTimer
16 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QTreeWidgetItem, \
17 QApplication, QHeaderView
18
19 from E5Gui.E5Application import e5App
20
21 from .Ui_SyntaxCheckerDialog import Ui_SyntaxCheckerDialog
22
23 import Utilities
24 import UI.PixmapCache
25
26
27 class SyntaxCheckerDialog(QDialog, Ui_SyntaxCheckerDialog):
28 """
29 Class implementing a dialog to display the results of a syntax check run.
30 """
31 filenameRole = Qt.UserRole + 1
32 lineRole = Qt.UserRole + 2
33 indexRole = Qt.UserRole + 3
34 errorRole = Qt.UserRole + 4
35 warningRole = Qt.UserRole + 5
36
37 def __init__(self, parent=None):
38 """
39 Constructor
40
41 @param parent The parent widget. (QWidget)
42 """
43 super(SyntaxCheckerDialog, self).__init__(parent)
44 self.setupUi(self)
45 self.setWindowFlags(Qt.Window)
46
47 self.showButton = self.buttonBox.addButton(
48 self.tr("Show"), QDialogButtonBox.ActionRole)
49 self.showButton.setToolTip(
50 self.tr("Press to show all files containing an issue"))
51 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
52 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
53
54 self.resultList.headerItem().setText(self.resultList.columnCount(), "")
55 self.resultList.header().setSortIndicator(0, Qt.AscendingOrder)
56
57 self.noResults = True
58 self.cancelled = False
59 self.__lastFileItem = None
60 self.__batch = False
61 self.__finished = True
62 self.__errorItem = None
63
64 self.__fileList = []
65 self.__project = None
66 self.filterFrame.setVisible(False)
67
68 self.checkProgress.setVisible(False)
69 self.checkProgressLabel.setVisible(False)
70 self.checkProgressLabel.setMaximumWidth(600)
71
72 try:
73 self.syntaxCheckService = e5App().getObject('SyntaxCheckService')
74 self.syntaxCheckService.syntaxChecked.connect(self.__processResult)
75 self.syntaxCheckService.batchFinished.connect(self.__batchFinished)
76 self.syntaxCheckService.error.connect(self.__processError)
77 except KeyError:
78 self.syntaxCheckService = None
79 self.filename = None
80
81 def __resort(self):
82 """
83 Private method to resort the tree.
84 """
85 self.resultList.sortItems(self.resultList.sortColumn(),
86 self.resultList.header().sortIndicatorOrder()
87 )
88
89 def __createErrorItem(self, filename, message):
90 """
91 Private slot to create a new error item in the result list.
92
93 @param filename name of the file
94 @type str
95 @param message error message
96 @type str
97 """
98 if self.__errorItem is None:
99 self.__errorItem = QTreeWidgetItem(self.resultList, [
100 self.tr("Errors")])
101 self.__errorItem.setExpanded(True)
102 self.__errorItem.setForeground(0, Qt.red)
103
104 msg = "{0} ({1})".format(self.__project.getRelativePath(filename),
105 message)
106 if not self.resultList.findItems(msg, Qt.MatchExactly):
107 itm = QTreeWidgetItem(self.__errorItem, [msg])
108 itm.setForeground(0, Qt.red)
109 itm.setFirstColumnSpanned(True)
110
111 def __createResultItem(self, filename, line, index, error, sourcecode,
112 isWarning=False):
113 """
114 Private method to create an entry in the result list.
115
116 @param filename file name of file (string)
117 @param line line number of faulty source (integer or string)
118 @param index index number of fault (integer)
119 @param error error text (string)
120 @param sourcecode faulty line of code (string)
121 @param isWarning flag indicating a warning message (boolean)
122 """
123 if self.__lastFileItem is None or \
124 self.__lastFileItem.data(0, self.filenameRole) != filename:
125 # It's a new file
126 self.__lastFileItem = QTreeWidgetItem(self.resultList, [
127 self.__project.getRelativePath(filename)])
128 self.__lastFileItem.setFirstColumnSpanned(True)
129 self.__lastFileItem.setExpanded(True)
130 self.__lastFileItem.setData(0, self.filenameRole, filename)
131
132 itm = QTreeWidgetItem(self.__lastFileItem)
133 if isWarning:
134 itm.setIcon(0, UI.PixmapCache.getIcon("warning.png"))
135 else:
136 itm.setIcon(0, UI.PixmapCache.getIcon("syntaxError.png"))
137 itm.setData(0, Qt.DisplayRole, line)
138 itm.setData(1, Qt.DisplayRole, error)
139 itm.setData(2, Qt.DisplayRole, sourcecode)
140 itm.setData(0, self.filenameRole, filename)
141 itm.setData(0, self.lineRole, int(line))
142 itm.setData(0, self.indexRole, index)
143 itm.setData(0, self.errorRole, error)
144 itm.setData(0, self.warningRole, isWarning)
145
146 def prepare(self, fileList, project):
147 """
148 Public method to prepare the dialog with a list of filenames.
149
150 @param fileList list of filenames (list of strings)
151 @param project reference to the project object (Project)
152 """
153 self.__fileList = fileList[:]
154 self.__project = project
155
156 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
157 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
158 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
159
160 self.filterFrame.setVisible(True)
161
162 self.__data = self.__project.getData("CHECKERSPARMS", "SyntaxChecker")
163 if self.__data is None or "ExcludeFiles" not in self.__data:
164 self.__data = {"ExcludeFiles": ""}
165 self.excludeFilesEdit.setText(self.__data["ExcludeFiles"])
166
167 def start(self, fn, codestring=""):
168 """
169 Public slot to start the syntax check.
170
171 @param fn file or list of files or directory to be checked
172 (string or list of strings)
173 @param codestring string containing the code to be checked (string).
174 If this is given, fn must be a single file name.
175 """
176 self.__batch = False
177
178 if self.syntaxCheckService is not None:
179 if self.__project is None:
180 self.__project = e5App().getObject("Project")
181
182 self.cancelled = False
183 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False)
184 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(True)
185 self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True)
186 self.showButton.setEnabled(False)
187 self.checkProgress.setVisible(True)
188 QApplication.processEvents()
189
190 if isinstance(fn, list):
191 self.files = fn
192 elif os.path.isdir(fn):
193 self.files = []
194 for ext in self.syntaxCheckService.getExtensions():
195 self.files.extend(
196 Utilities.direntries(fn, True, '*{0}'.format(ext), 0))
197 else:
198 self.files = [fn]
199
200 self.__errorItem = None
201 self.__clearErrors(self.files)
202
203 if codestring or len(self.files) > 0:
204 self.checkProgress.setMaximum(max(1, len(self.files)))
205 self.checkProgress.setVisible(len(self.files) > 1)
206 self.checkProgressLabel.setVisible(len(self.files) > 1)
207 QApplication.processEvents()
208
209 # now go through all the files
210 self.progress = 0
211 self.files.sort()
212 if codestring or len(self.files) == 1:
213 self.__batch = False
214 self.check(codestring)
215 else:
216 self.__batch = True
217 self.checkBatch()
218
219 def check(self, codestring=''):
220 """
221 Public method to start a check for one file.
222
223 The results are reported to the __processResult slot.
224 @keyparam codestring optional sourcestring (str)
225 """
226 if self.syntaxCheckService is None or not self.files:
227 self.checkProgressLabel.setPath("")
228 self.checkProgress.setMaximum(1)
229 self.checkProgress.setValue(1)
230 self.__finish()
231 return
232
233 self.filename = self.files.pop(0)
234 self.checkProgress.setValue(self.progress)
235 self.checkProgressLabel.setPath(self.filename)
236 QApplication.processEvents()
237 self.__resort()
238
239 if self.cancelled:
240 return
241
242 self.__lastFileItem = None
243
244 if codestring:
245 self.source = codestring
246 else:
247 try:
248 self.source = Utilities.readEncodedFile(self.filename)[0]
249 self.source = Utilities.normalizeCode(self.source)
250 except (UnicodeError, IOError) as msg:
251 self.noResults = False
252 self.__createResultItem(
253 self.filename, 1, 0,
254 self.tr("Error: {0}").format(str(msg))
255 .rstrip(), "")
256 self.progress += 1
257 # Continue with next file
258 self.check()
259 return
260
261 self.__finished = False
262 self.syntaxCheckService.syntaxCheck(None, self.filename, self.source)
263
264 def checkBatch(self):
265 """
266 Public method to start a style check batch job.
267
268 The results are reported to the __processResult slot.
269 """
270 self.__lastFileItem = None
271
272 self.checkProgressLabel.setPath(self.tr("Preparing files..."))
273 progress = 0
274
275 argumentsList = []
276 for filename in self.files:
277 progress += 1
278 self.checkProgress.setValue(progress)
279 QApplication.processEvents()
280
281 try:
282 source = Utilities.readEncodedFile(filename)[0]
283 source = Utilities.normalizeCode(source)
284 except (UnicodeError, IOError) as msg:
285 self.noResults = False
286 self.__createResultItem(
287 self.filename, 1, 0,
288 self.tr("Error: {0}").format(str(msg))
289 .rstrip(), "")
290 continue
291
292 argumentsList.append((filename, source))
293
294 # reset the progress bar to the checked files
295 self.checkProgress.setValue(self.progress)
296 self.checkProgressLabel.setPath(self.tr("Transferring data..."))
297 QApplication.processEvents()
298
299 self.__finished = False
300 self.syntaxCheckService.syntaxBatchCheck(argumentsList)
301
302 def __batchFinished(self):
303 """
304 Private slot handling the completion of a batch job.
305 """
306 self.checkProgressLabel.setPath("")
307 self.checkProgress.setMaximum(1)
308 self.checkProgress.setValue(1)
309 self.__finish()
310
311 def __processError(self, fn, msg):
312 """
313 Private slot to process an error indication from the service.
314
315 @param fn filename of the file
316 @type str
317 @param msg error message
318 @type str
319 """
320 self.__createErrorItem(fn, msg)
321
322 if not self.__batch:
323 self.check()
324
325 def __processResult(self, fn, problems):
326 """
327 Private slot to display the reported messages.
328
329 @param fn filename of the checked file (str)
330 @param problems dictionary with the keys 'error' and 'warnings' which
331 hold a list containing details about the error/ warnings
332 (file name, line number, column, codestring (only at syntax
333 errors), the message) (dict)
334 """
335 if self.__finished:
336 return
337
338 # Check if it's the requested file, otherwise ignore signal if not
339 # in batch mode
340 if not self.__batch and fn != self.filename:
341 return
342
343 error = problems.get('error')
344 if error:
345 self.noResults = False
346 _fn, lineno, col, code, msg = error
347 self.__createResultItem(_fn, lineno, col, msg, code, False)
348
349 warnings = problems.get('warnings', [])
350 if warnings:
351 if self.__batch:
352 try:
353 source = Utilities.readEncodedFile(fn)[0]
354 source = Utilities.normalizeCode(source)
355 source = source.splitlines()
356 except (UnicodeError, IOError):
357 source = ""
358 else:
359 source = self.source.splitlines()
360 for filename, lineno, col, _code, msg in warnings:
361 self.noResults = False
362 if source:
363 try:
364 scr_line = source[lineno - 1].strip()
365 except IndexError:
366 scr_line = ""
367 else:
368 scr_line = ""
369 self.__createResultItem(filename, lineno, col, msg, scr_line,
370 True)
371
372 self.progress += 1
373 self.checkProgress.setValue(self.progress)
374 self.checkProgressLabel.setPath(fn)
375 QApplication.processEvents()
376 self.__resort()
377
378 if not self.__batch:
379 self.check()
380
381 def __finish(self):
382 """
383 Private slot called when the syntax check finished or the user
384 pressed the button.
385 """
386 if not self.__finished:
387 self.__finished = True
388
389 self.cancelled = True
390 self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True)
391 self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False)
392 self.buttonBox.button(QDialogButtonBox.Close).setDefault(True)
393
394 if self.noResults:
395 QTreeWidgetItem(self.resultList, [self.tr('No issues found.')])
396 QApplication.processEvents()
397 self.showButton.setEnabled(False)
398 else:
399 self.showButton.setEnabled(True)
400 self.resultList.header().resizeSections(
401 QHeaderView.ResizeToContents)
402 self.resultList.header().setStretchLastSection(True)
403
404 self.checkProgress.setVisible(False)
405 self.checkProgressLabel.setVisible(False)
406
407 def on_buttonBox_clicked(self, button):
408 """
409 Private slot called by a button of the button box clicked.
410
411 @param button button that was clicked (QAbstractButton)
412 """
413 if button == self.buttonBox.button(QDialogButtonBox.Close):
414 self.close()
415 elif button == self.buttonBox.button(QDialogButtonBox.Cancel):
416 if self.__batch:
417 self.syntaxCheckService.cancelSyntaxBatchCheck()
418 QTimer.singleShot(1000, self.__finish)
419 else:
420 self.__finish()
421 elif button == self.showButton:
422 self.on_showButton_clicked()
423
424 @pyqtSlot()
425 def on_startButton_clicked(self):
426 """
427 Private slot to start a syntax check run.
428 """
429 fileList = self.__fileList[:]
430
431 filterString = self.excludeFilesEdit.text()
432 if "ExcludeFiles" not in self.__data or \
433 filterString != self.__data["ExcludeFiles"]:
434 self.__data["ExcludeFiles"] = filterString
435 self.__project.setData("CHECKERSPARMS", "SyntaxChecker",
436 self.__data)
437 filterList = [f.strip() for f in filterString.split(",")
438 if f.strip()]
439 if filterList:
440 for fileFilter in filterList:
441 fileList = \
442 [f for f in fileList if not fnmatch.fnmatch(f, fileFilter)]
443
444 self.resultList.clear()
445 self.noResults = True
446 self.cancelled = False
447 self.start(fileList)
448
449 def on_resultList_itemActivated(self, itm, col):
450 """
451 Private slot to handle the activation of an item.
452
453 @param itm reference to the activated item (QTreeWidgetItem)
454 @param col column the item was activated in (integer)
455 """
456 if self.noResults:
457 return
458
459 vm = e5App().getObject("ViewManager")
460
461 if itm.parent():
462 fn = Utilities.normabspath(itm.data(0, self.filenameRole))
463 lineno = itm.data(0, self.lineRole)
464 index = itm.data(0, self.indexRole)
465 error = itm.data(0, self.errorRole)
466
467 vm.openSourceFile(fn, lineno)
468 editor = vm.getOpenEditor(fn)
469
470 if itm.data(0, self.warningRole):
471 editor.toggleWarning(lineno, 0, True, error)
472 else:
473 editor.toggleSyntaxError(lineno, index, True, error, show=True)
474 else:
475 fn = Utilities.normabspath(itm.data(0, self.filenameRole))
476 vm.openSourceFile(fn)
477 editor = vm.getOpenEditor(fn)
478 for index in range(itm.childCount()):
479 citm = itm.child(index)
480 lineno = citm.data(0, self.lineRole)
481 index = citm.data(0, self.indexRole)
482 error = citm.data(0, self.errorRole)
483 if citm.data(0, self.warningRole):
484 editor.toggleWarning(lineno, 0, True, error)
485 else:
486 editor.toggleSyntaxError(
487 lineno, index, True, error, show=True)
488
489 editor = vm.activeWindow()
490 editor.updateVerticalScrollBar()
491
492 @pyqtSlot()
493 def on_showButton_clicked(self):
494 """
495 Private slot to handle the "Show" button press.
496 """
497 vm = e5App().getObject("ViewManager")
498
499 selectedIndexes = []
500 for index in range(self.resultList.topLevelItemCount()):
501 if self.resultList.topLevelItem(index).isSelected():
502 selectedIndexes.append(index)
503 if len(selectedIndexes) == 0:
504 selectedIndexes = list(range(self.resultList.topLevelItemCount()))
505 for index in selectedIndexes:
506 itm = self.resultList.topLevelItem(index)
507 fn = Utilities.normabspath(itm.data(0, self.filenameRole))
508 vm.openSourceFile(fn, 1)
509 editor = vm.getOpenEditor(fn)
510 editor.clearSyntaxError()
511 editor.clearFlakesWarnings()
512 for cindex in range(itm.childCount()):
513 citm = itm.child(cindex)
514 lineno = citm.data(0, self.lineRole)
515 index = citm.data(0, self.indexRole)
516 error = citm.data(0, self.errorRole)
517 if citm.data(0, self.warningRole):
518 editor.toggleWarning(lineno, 0, True, error)
519 else:
520 editor.toggleSyntaxError(
521 lineno, index, True, error, show=True)
522
523 # go through the list again to clear syntax error and
524 # flakes warning markers for files, that are ok
525 openFiles = vm.getOpenFilenames()
526 errorFiles = []
527 for index in range(self.resultList.topLevelItemCount()):
528 itm = self.resultList.topLevelItem(index)
529 errorFiles.append(
530 Utilities.normabspath(itm.data(0, self.filenameRole)))
531 for file in openFiles:
532 if file not in errorFiles:
533 editor = vm.getOpenEditor(file)
534 editor.clearSyntaxError()
535 editor.clearFlakesWarnings()
536
537 editor = vm.activeWindow()
538 editor.updateVerticalScrollBar()
539
540 def __clearErrors(self, files):
541 """
542 Private method to clear all error and warning markers of
543 open editors to be checked.
544
545 @param files list of files to be checked (list of string)
546 """
547 vm = e5App().getObject("ViewManager")
548 openFiles = vm.getOpenFilenames()
549 for file in [f for f in openFiles if f in files]:
550 editor = vm.getOpenEditor(file)
551 editor.clearSyntaxError()
552 editor.clearFlakesWarnings()

eric ide

mercurial