src/eric7/Plugins/CheckerPlugins/SyntaxChecker/SyntaxCheckerDialog.py

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

eric ide

mercurial