diff --git a/.printer-linter b/.printer-linter index 9724c63300..228cc3c97f 100644 --- a/.printer-linter +++ b/.printer-linter @@ -4,6 +4,7 @@ checks: diagnostic-definition-redundant-override: true diagnostic-resources-macos-app-directory-name: true diagnostic-resource-file-deleted: true + diagnostic-incorrect-formula: true diagnostic-material-temperature-defined: true fixes: diagnostic-definition-redundant-override: true diff --git a/printer-linter/src/printerlinter/factory.py b/printer-linter/src/printerlinter/factory.py index 0d706788fc..37a11d471a 100644 --- a/printer-linter/src/printerlinter/factory.py +++ b/printer-linter/src/printerlinter/factory.py @@ -6,6 +6,7 @@ from .linters.defintion import Definition from .linters.linter import Linter from .linters.meshes import Meshes from .linters.directory import Directory +from .linters.formulas import Formulas def getLinter(file: Path, settings: dict) -> Optional[List[Linter]]: @@ -14,12 +15,12 @@ def getLinter(file: Path, settings: dict) -> Optional[List[Linter]]: return [Directory(file, settings)] if ".inst" in file.suffixes and ".cfg" in file.suffixes: - return [Directory(file, settings), Profile(file, settings)] + return [Directory(file, settings), Profile(file, settings), Formulas(file, settings)] if ".def" in file.suffixes and ".json" in file.suffixes: if file.stem in ("fdmprinter.def", "fdmextruder.def"): - return None - return [Directory(file, settings), Definition(file, settings)] + return [Formulas(file, settings)] + return [Directory(file, settings), Definition(file, settings), Formulas(file, settings)] if file.parent.stem == "meshes": return [Meshes(file, settings)] diff --git a/printer-linter/src/printerlinter/linters/formulas.py b/printer-linter/src/printerlinter/linters/formulas.py new file mode 100644 index 0000000000..53a059f7d2 --- /dev/null +++ b/printer-linter/src/printerlinter/linters/formulas.py @@ -0,0 +1,155 @@ +import difflib +import json +import os +import re +from configparser import ConfigParser +from pathlib import Path +from typing import Iterator +from unittest.mock import MagicMock + +from UM.Settings.DefinitionContainer import DefinitionContainer +from UM.VersionUpgradeManager import VersionUpgradeManager +from cura.CuraApplication import CuraApplication +from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions + +from ..diagnostic import Diagnostic +from ..replacement import Replacement +from .linter import Linter + +FORMULA_NAMES = [ + "extruderValue", "extruderValues", "anyExtruderWithMaterial", "anyExtruderNrWithOrDefault", + "resolveOrValue", "defaultExtruderPosition", "valueFromContainer", "extruderValueFromContainer" +] + +DELIMITERS = [r'\+', '-', '=', '/', '\*', r'\(', r'\)', r'\[', r'\]', '{', '}', ' ', '^'] + + +class Formulas(Linter): + """Finds issues in definition files, such as overriding default parameters.""" + + def __init__(self, file: Path, settings: dict) -> None: + super().__init__(file, settings) + self._cura_correction_strings = FORMULA_NAMES + list(self.getCuraSettingList()) + self._definition = {} + + def getCuraSettingList(self) -> list: + if VersionUpgradeManager._VersionUpgradeManager__instance is None: + VersionUpgradeManager._VersionUpgradeManager__instance = VersionUpgradeManager(MagicMock()) + CuraApplication._initializeSettingDefinitions() + definition_container = DefinitionContainer("whatever") + with open(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", "resources", "definitions", "fdmprinter.def.json"), encoding="utf-8") as data: + definition_container.deserialize(data.read()) + return definition_container.getAllKeys() + + def check(self) -> Iterator[Diagnostic]: + if self._settings["checks"].get("diagnostic-incorrect-formula", False): + for check in self.checkFormulas(): + yield check + yield + + def checkFormulas(self) -> Iterator[Diagnostic]: + + self._loadDefinitionFiles(self._file) + self._content = self._file.read_text() + definition_name = list(self._definition.keys())[0] + definition = self._definition[definition_name] + if "overrides" in definition: + for key, value_dict in definition["overrides"].items(): + for value in value_dict: + if value in ("enable", "resolve", "value", "minimum_value_warning", "maximum_value_warning", + "maximum_value", "minimum_value"): + key_incorrect = self.checkValueIncorrect(key) + if key_incorrect: + found = self._appendCorrections(key, key) + value_incorrect = self.checkValueIncorrect(value_dict[value]) + if value_incorrect: + found = self._appendCorrections(key, value_dict[value]) + if key_incorrect or value_incorrect: + + if len(found.group().splitlines()) > 1: + replacements = [] + else: + replacements = [Replacement( + file=self._file, + offset=found.span(1)[0], + length=len(found.group()), + replacement_text=self._replacement_text)] + yield Diagnostic( + file=self._file, + diagnostic_name="diagnostic-incorrect-formula", + message=f"Given formula {found.group()} seems incorrect, Do you mean {self._correct_formula}? please correct the formula and try again.", + level="Error", + offset=found.span(0)[0], + replacements=replacements + ) + + yield + + def _appendCorrections(self, key, incorrectString): + + if self._file.suffix == '.cfg': + key_with_incorrectValue = re.compile(r'(\b' + key + r'\b\s*=\s*[^=\n]+.*)') + else: + key_with_incorrectValue = re.compile(r'.*(\"' + key + r'\"[\s\:\S]*?)\{[\s\S]*?\},?') + found = key_with_incorrectValue.search(self._content) + if len(found.group().splitlines()) > 1: + self._replacement_text = '' + else: + self._replacement_text = found.group().replace(incorrectString, self._correct_formula) + return found + + + def _loadDefinitionFiles(self, definition_file) -> None: + """ Loads definition file contents into self._definition. Also load parent definition if it exists. """ + definition_name = Path(definition_file.stem).stem + + if not definition_file.exists() or definition_name in self._definition: + return + + if definition_file.suffix == ".json": + # Load definition file into dictionary + self._definition[definition_name] = json.loads(definition_file.read_text()) + + if definition_file.suffix == ".cfg": + self._definition[definition_name] = self._parseCfg(definition_file) + + + def _parseCfg(self, file_path:Path) -> dict: + config = ConfigParser() + config.read([file_path]) + file_data ={} + overrides = {} + + available_sections = ["values"] + for section in available_sections: + options = config.options(section) + for option in options: + values ={} + values["value"] = config.get(section, option) + overrides[option] = values + file_data["overrides"]= overrides# Process the value here + + return file_data + + def checkValueIncorrect(self, formula) -> bool: + if isinstance(formula, str): + self._correct_formula = self._correctTyposInFormula(formula) + if self._correct_formula == formula: + return False + return True + else: + return False + + def _correctTyposInFormula(self, formula): + pattern = '|'.join(DELIMITERS) + tokens = re.split(pattern, formula) + + output = formula + for token in tokens: + if '(' not in token and ')' not in token: + cleaned_token = re.sub(r'[^\w\s]', '', token) + possible_matches = difflib.get_close_matches(cleaned_token, self._cura_correction_strings, n=1, cutoff=0.8) + if possible_matches: + output = output.replace(cleaned_token, possible_matches[0]) + return output +