eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py

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

eric ide

mercurial