diff --git a/HISTORY.rst b/HISTORY.rst index 9da4a370..c5b2c46e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -18,6 +18,7 @@ PlatformIO Core 6 6.1.8 (2023-??-??) ~~~~~~~~~~~~~~~~~~ +* Added a new ``--lint`` option to the `pio project config `__ command, enabling users to efficiently perform linting on the |PIOCONF| * Enhanced the parsing of the |PIOCONF| to provide comprehensive diagnostic information * Optimized project integration templates to address the issue of long paths on Windows (`issue #4652 `_) * Refactored |UNITTESTING| engine to resolve compiler warnings with "-Wpedantic" option (`pull #4671 `_) diff --git a/platformio/project/commands/config.py b/platformio/project/commands/config.py index 9f218e80..5e4ed21f 100644 --- a/platformio/project/commands/config.py +++ b/platformio/project/commands/config.py @@ -30,16 +30,23 @@ from platformio.project.helpers import is_platformio_project default=os.getcwd, type=click.Path(exists=True, file_okay=False, dir_okay=True), ) +@click.option("--lint", is_flag=True) @click.option("--json-output", is_flag=True) -def project_config_cmd(project_dir, json_output): +def project_config_cmd(project_dir, lint, json_output): if not is_platformio_project(project_dir): raise NotPlatformIOProjectError(project_dir) with fs.cd(project_dir): - config = ProjectConfig.get_instance() + if lint: + return lint_configuration(json_output) + return print_configuration(json_output) + + +def print_configuration(json_output=False): + config = ProjectConfig.get_instance() if json_output: return click.echo(config.to_json()) click.echo( - "Computed project configuration for %s" % click.style(project_dir, fg="cyan") + "Computed project configuration for %s" % click.style(os.getcwd(), fg="cyan") ) for section, options in config.as_tuple(): click.secho(section, fg="cyan") @@ -55,3 +62,43 @@ def project_config_cmd(project_dir, json_output): ) click.echo() return None + + +def lint_configuration(json_output=False): + result = ProjectConfig.lint() + errors = result["errors"] + warnings = result["warnings"] + if json_output: + return click.echo(result) + if not errors and not warnings: + return click.secho( + 'The "platformio.ini" configuration file is free from linting errors.', + fg="green", + ) + if errors: + click.echo( + tabulate( + [ + ( + click.style(error["type"], fg="red"), + error["message"], + error.get("source", "") + (f":{error.get('lineno')}") + if "lineno" in error + else "", + ) + for error in errors + ], + tablefmt="plain", + ) + ) + if warnings: + click.echo( + tabulate( + [ + (click.style("Warning", fg="yellow"), warning) + for warning in warnings + ], + tablefmt="plain", + ) + ) + return None diff --git a/platformio/project/config.py b/platformio/project/config.py index 08ab422c..4658acfa 100644 --- a/platformio/project/config.py +++ b/platformio/project/config.py @@ -433,7 +433,41 @@ class ProjectConfigDirsMixin: return self.get("platformio", f"{name}_dir") -class ProjectConfig(ProjectConfigBase, ProjectConfigDirsMixin): +class ProjectConfigLintMixin: + @classmethod + def lint(cls, path=None): + errors = [] + warnings = [] + try: + config = cls.get_instance(path) + config.validate(silent=True) + warnings = config.warnings + config.as_tuple() + except Exception as exc: # pylint: disable=broad-exception-caught + if exc.__cause__ is not None: + exc = exc.__cause__ + + item = {"type": exc.__class__.__name__, "message": str(exc)} + for attr in ("lineno", "source"): + if hasattr(exc, attr): + item[attr] = getattr(exc, attr) + + if item["type"] == "ParsingError" and hasattr(exc, "errors"): + for lineno, line in getattr(exc, "errors"): + errors.append( + { + "type": item["type"], + "message": f"Parsing error: {line}", + "lineno": lineno, + "source": item["source"], + } + ) + else: + errors.append(item) + return {"errors": errors, "warnings": warnings} + + +class ProjectConfig(ProjectConfigBase, ProjectConfigDirsMixin, ProjectConfigLintMixin): _instances = {} @staticmethod diff --git a/tests/project/test_config.py b/tests/project/test_config.py index f559502e..6b742855 100644 --- a/tests/project/test_config.py +++ b/tests/project/test_config.py @@ -684,3 +684,34 @@ def test_invalid_env_names(tmp_path: Path): config = ProjectConfig(str(project_conf)) with pytest.raises(InvalidEnvNameError, match=r".*Invalid environment name 'app:1"): config.validate() + + +def test_linting_errors(tmp_path: Path): + project_conf = tmp_path / "platformio.ini" + project_conf.write_text( + """ +[env:app1] +lib_use = 1 +broken_line + """ + ) + result = ProjectConfig.lint(str(project_conf)) + assert not result["warnings"] + assert result["errors"] and len(result["errors"]) == 1 + error = result["errors"][0] + assert error["type"] == "ParsingError" + assert error["lineno"] == 4 + + +def test_linting_warnings(tmp_path: Path): + project_conf = tmp_path / "platformio.ini" + project_conf.write_text( + """ +[env:app1] +lib_use = 1 + """ + ) + result = ProjectConfig.lint(str(project_conf)) + assert not result["errors"] + assert result["warnings"] and len(result["warnings"]) == 1 + assert "deprecated" in result["warnings"][0]