--- a/VultureChecker/VultureCheckerDialog.py Sun Oct 04 18:40:19 2015 +0200 +++ b/VultureChecker/VultureCheckerDialog.py Mon Oct 05 19:40:51 2015 +0200 @@ -17,10 +17,10 @@ import os import fnmatch -from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QLocale +from PyQt5.QtCore import pyqtSlot, qVersion, Qt, QTimer, QRegExp from PyQt5.QtWidgets import ( QDialog, QDialogButtonBox, QAbstractButton, QHeaderView, QTreeWidgetItem, - QApplication + QApplication, QMenu ) from .Ui_VultureCheckerDialog import Ui_VultureCheckerDialog @@ -33,12 +33,12 @@ from .vulture import Item -# TODO: add a whitelist (incl. context menu to add an entry to the list) class VultureCheckerDialog(QDialog, Ui_VultureCheckerDialog): """ Class implementing a dialog to show the vulture check results. """ FilePathRole = Qt.UserRole + 1 + TypeRole = Qt.UserRole + 2 def __init__(self, vultureService, parent=None): """ @@ -58,6 +58,20 @@ self.resultList.headerItem().setText(self.resultList.columnCount(), "") + self.__menu = QMenu(self) + self.__whiteListAct = self.__menu.addAction( + self.tr("Add to Whitelist"), self.__whiteList) + self.__menu.addSeparator() + self.__menu.addAction( + self.tr("Edit Whitelist"), self.__editWhiteList) + self.__menu.addSeparator() + self.__collapseAct = self.__menu.addAction( + self.tr("Collapse all"), self.__resultCollapse) + self.__expandAct = self.__menu.addAction( + self.tr("Expand all"), self.__resultExpand) + self.resultList.customContextMenuRequested.connect( + self.__showContextMenu) + self.vultureService = vultureService self.vultureService.analysisDone.connect(self.__processResult) self.vultureService.error.connect(self.__processError) @@ -66,19 +80,20 @@ self.cancelled = False self.__project = e5App().getObject("Project") - self.__locale = QLocale() self.__finished = True self.__errorItem = None + self.__data = None self.__fileList = [] self.filterFrame.setVisible(False) - - def __resizeResultColumns(self): - """ - Private method to resize the list columns. - """ - self.resultList.header().resizeSections(QHeaderView.ResizeToContents) - self.resultList.header().setStretchLastSection(True) + + self.__translatedTypes = { + "property": self.tr("Property"), + "function": self.tr("Function"), + "attribute": self.tr("Attribute"), + "variable": self.tr("Variable"), + "class": self.tr("Class"), + } def __createErrorItem(self, filename, message): """ @@ -122,8 +137,22 @@ self.__data = self.__project.getData( "CHECKERSPARMS", "Vulture") - if self.__data is None or "ExcludeFiles" not in self.__data: - self.__data = {"ExcludeFiles": ""} + if self.__data is None: + self.__data = {} + if "ExcludeFiles" not in self.__data: + self.__data["ExcludeFiles"] = "" + if "WhiteLists" not in self.__data: + self.__data["WhiteLists"] = { + "property": [], + "function": [], + "attribute": [], + "variable": [], + "class": [], + "__patterns__": [ + "on_*", + "visit_*", + ], + } self.excludeFilesEdit.setText(self.__data["ExcludeFiles"]) def start(self, fn): @@ -226,8 +255,6 @@ The results are reported to the __processResult slot. """ - self.__lastFileItem = None - self.checkProgressLabel.setPath(self.tr("Preparing files...")) progress = 0 @@ -364,8 +391,7 @@ fileList = self.__fileList[:] filterString = self.excludeFilesEdit.text() - if "ExcludeFiles" not in self.__data or \ - filterString != self.__data["ExcludeFiles"]: + if filterString != self.__data["ExcludeFiles"]: self.__data["ExcludeFiles"] = filterString self.__project.setData( "CHECKERSPARMS", "Vulture", self.__data) @@ -424,14 +450,14 @@ @param result result dictionary @type dict """ - self.__definedAttrs.extend( - [self.__dict2Item(d) for d in result["DefinedAttributes"]]) - self.__definedFuncs.extend( - [self.__dict2Item(d) for d in result["DefinedFunctions"]]) - self.__definedProps.extend( - [self.__dict2Item(d) for d in result["DefinedProperties"]]) - self.__definedVars.extend( - [self.__dict2Item(d) for d in result["DefinedVariables"]]) + self.__definedAttrs.extend(self.__filteredList( + [self.__dict2Item(d) for d in result["DefinedAttributes"]])) + self.__definedFuncs.extend(self.__filteredList( + [self.__dict2Item(d) for d in result["DefinedFunctions"]])) + self.__definedProps.extend(self.__filteredList( + [self.__dict2Item(d) for d in result["DefinedProperties"]])) + self.__definedVars.extend(self.__filteredList( + [self.__dict2Item(d) for d in result["DefinedVariables"]])) self.__usedAttrs.extend( [self.__dict2Item(d) for d in result["UsedAttributes"]]) self.__usedVars.extend( @@ -452,6 +478,23 @@ """ return Item(d["name"], d["type"], d["file"], d["line"]) + def __filteredList(self, itemList): + """ + Private method to filter a list against the whitelist patterns + returning items not matching the whitelist. + + @param itemList list of items to be filtered + @type list of vulture.Item + @return list of filtered items + @rtype list of vulture.Item + """ + filteredList = itemList + for pattern in self.__data["WhiteLists"]["__patterns__"]: + regExp = QRegExp(pattern, Qt.CaseSensitive, QRegExp.Wildcard) + filteredList = [name for name in filteredList + if not regExp.exactMatch(name)] + return filteredList + def __getUnusedItems(self, defined, used): """ Private method to get a list of unused items. @@ -474,7 +517,10 @@ """ return self.__getUnusedItems( self.__definedFuncs, - self.__usedAttrs + self.__usedVars + self.__namesImportedAsAliases) + self.__usedAttrs + self.__usedVars + + self.__namesImportedAsAliases + + self.__data["WhiteLists"]["function"] + + self.__data["WhiteLists"]["class"]) def __unusedProperties(self): """ @@ -483,7 +529,9 @@ @return list of unused properties @rtype list of vulture.Item """ - return self.__getUnusedItems(self.__definedProps, self.__usedAttrs) + return self.__getUnusedItems( + self.__definedProps, + self.__usedAttrs + self.__data["WhiteLists"]["property"]) def __unusedVariables(self): """ @@ -495,7 +543,8 @@ return self.__getUnusedItems( self.__definedVars, self.__usedAttrs + self.__usedVars + self.__tupleAssignVars + - self.__namesImportedAsAliases) + self.__namesImportedAsAliases + + self.__data["WhiteLists"]["variable"]) def __unusedAttributes(self): """ @@ -504,30 +553,153 @@ @return list of unused attributes @rtype list of vulture.Item """ - return self.__getUnusedItems(self.__definedAttrs, self.__usedAttrs) + return self.__getUnusedItems( + self.__definedAttrs, + self.__usedAttrs + self.__data["WhiteLists"]["attribute"]) def __createResultItems(self): """ Private method to populate the list with the analysis result. - """ + """ # __IGNORE_WARNING__ def filename(item): return item.file lastFileItem = None lastFileName = "" - for item in sorted(self.__unusedFunctions() + + for item in sorted(self.__unusedFunctions() + self.__unusedProperties() + - self.__unusedVariables() + + self.__unusedVariables() + self.__unusedAttributes(), key=filename): if lastFileItem is None or lastFileName != item.file: - lastFileItem = QTreeWidgetItem(self.resultList, [ - self.__project.getRelativePath(item.file)]) - lastFileItem.setData(0, self.FilePathRole, item.file) - lastFileItem.setExpanded(True) - lastFileItem.setFirstColumnSpanned(True) + lastFileItem = self.__createFileItem(item.file) lastFileName = item.file - itm = QTreeWidgetItem(lastFileItem, [ - "{0:6d}".format(item.lineno), str(item), item.typ]) - itm.setData(0, self.FilePathRole, item.file) + self.__createResultItem(lastFileItem, item) + + def __createResultItem(self, parent, item): + """ + Private method to create a result item. + + @param parent reference to the parent item + @type QTreeWidgetItem + @param item reference to the item + @type vulture.Item + """ + try: + translatedType = self.__translatedTypes[item.typ] + except KeyError: + translatedType = item.typ + itm = QTreeWidgetItem(parent, [ + "{0:6d}".format(item.lineno), str(item), translatedType]) + itm.setData(0, self.FilePathRole, item.file) + itm.setData(0, self.TypeRole, item.typ) + itm.setTextAlignment(0, Qt.Alignment(Qt.AlignRight)) + + def __createFileItem(self, filename): + """ + Private method to create a file item. + + @param filename file name for the item + @type str + """ + itm = QTreeWidgetItem(self.resultList, [ + self.__project.getRelativePath(filename)]) + itm.setData(0, self.FilePathRole, filename) + itm.setExpanded(True) + itm.setFirstColumnSpanned(True) + + return itm + + def __showContextMenu(self, coord): + """ + Private slot to show the context menu of the listview. + + @param coord the position of the mouse pointer + @type QPoint + """ + topLevelPresent = self.resultList.topLevelItemCount() > 0 + self.__collapseAct.setEnabled(topLevelPresent) + self.__expandAct.setEnabled(topLevelPresent) + + self.__whiteListAct.setEnabled( + len(self.__getSelectedNonFileItems()) != 0) + + self.__menu.popup(self.resultList.mapToGlobal(coord)) + + def __resultCollapse(self): + """ + Private slot to collapse all entries of the resultlist. + """ + for index in range(self.resultList.topLevelItemCount()): + self.resultList.topLevelItem(index).setExpanded(False) + + def __resultExpand(self): + """ + Private slot to expand all entries of the resultlist. + """ + for index in range(self.resultList.topLevelItemCount()): + self.resultList.topLevelItem(index).setExpanded(True) + + def __getSelectedNonFileItems(self): + """ + Private method to get a list of selected non file items. + + @return list of selected non file items + @rtype list of QTreeWidgetItem + """ + itmList = [i for i in self.resultList.selectedItems() + if i.parent() is not None] + return itmList + + def __editWhiteList(self): + """ + Private slot to edit the whitelist. + """ + # TODO: add a whitelist edit dialog with "add" and "delete" +## whitelist = dlg.getWhiteList() +## self.__storeWhiteList(whitelist) + + def __whiteList(self): + """ + Private slot to add entries to the whitelist. + """ + whitelists = {} + for key in self.__data["WhiteLists"]: + whitelists[key] = self.__data["WhiteLists"][key][:] + for itm in self.__getSelectedNonFileItems(): + try: + whitelists[itm.data(0, self.TypeRole)].append(itm.text(1)) + except KeyError: + # ignore non-existing types + pass + # remove the item from the result list + pitm = itm.parent() + pitm.removeChild(itm) + del itm + if pitm.childCount() == 0: + self.resultList.takeTopLevelItem( + self.resultList.indexOfTopLevelItem(pitm)) + del pitm + self.__storeWhiteList(whitelists) + + def __storeWhiteList(self, whitelists): + """ + Private method to store the new whitelists, if they have changed. + + @param whitelists dictionary of lists of whitelisted names + @type dict of list of str + """ + changed = False + for key in whitelists: + whitelist = list(set(whitelists[key])) + try: + if sorted(whitelist) != sorted(self.__data["WhiteLists"][key]): + self.__data["WhiteLists"][key] = whitelist[:] + changed = True + except KeyError: + # ignore non-existing types + pass + + if changed: + self.__project.setData("CHECKERSPARMS", "Vulture", self.__data)