From d17ee50a4c93c7a6b6f3f9de68cfd909fb8215f0 Mon Sep 17 00:00:00 2001 From: Reiner Gerecke Date: Tue, 27 Dec 2022 16:59:54 +0100 Subject: [PATCH] Expose check method to python Refs: #659 --- ruff/__init__.py | 1 + ruff/__init__.pyi | 12 +++++ ruff/py.typed | 0 ruff/tests.py | 26 +++++++++++ src/lib_python.rs | 110 +++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 ruff/__init__.pyi create mode 100644 ruff/py.typed create mode 100644 ruff/tests.py diff --git a/ruff/__init__.py b/ruff/__init__.py index e69de29bb2d1d..af20d59c8585c 100644 --- a/ruff/__init__.py +++ b/ruff/__init__.py @@ -0,0 +1 @@ +from ._ruff import check # noqa: F401 diff --git a/ruff/__init__.pyi b/ruff/__init__.pyi new file mode 100644 index 0000000000000..71887144707c6 --- /dev/null +++ b/ruff/__init__.pyi @@ -0,0 +1,12 @@ +class Location: + row: int + column: int + + +class Message: + code: str + message: str + location: Location + end_location: Location + +def check(contents: str | None, path: str | None) -> list(Message): ... \ No newline at end of file diff --git a/ruff/py.typed b/ruff/py.typed new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/ruff/tests.py b/ruff/tests.py new file mode 100644 index 0000000000000..e5372863ac8e8 --- /dev/null +++ b/ruff/tests.py @@ -0,0 +1,26 @@ +import unittest + +from ruff import check + + +class CheckTest(unittest.TestCase): + def test_something(self): + results = check( + """ +import os +import sys + +print(os.environ['GREETING']) + """ + ) + self.assertEqual(len(results), 1) + self.assertEqual(results[0].code, "F401") + self.assertEqual(results[0].message, "`sys` imported but unused") + self.assertEqual(results[0].location.row, 3) + self.assertEqual(results[0].location.column, 7) + self.assertEqual(results[0].end_location.row, 3) + self.assertEqual(results[0].end_location.column, 10) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/lib_python.rs b/src/lib_python.rs index ea8e31947b7d6..2c4e96dd6fd4f 100644 --- a/src/lib_python.rs +++ b/src/lib_python.rs @@ -1,6 +1,114 @@ +use std::path::Path; + +use anyhow::Result; use pyo3::prelude::*; +use pyo3::types::PyString; +use rustpython_parser::lexer::LexResult; + +use crate::checks::{Check, CheckCode}; +use crate::linter::check_path; +use crate::rustpython_helpers::tokenize; +use crate::settings::configuration::Configuration; +use crate::settings::options::Options; +use crate::settings::{flags, Settings}; +use crate::source_code_locator::SourceCodeLocator; +use crate::source_code_style::SourceCodeStyleDetector; +use crate::{directives, packages}; + +#[pyclass] +#[derive(Clone)] +struct Location { + #[pyo3(get)] + row: usize, + #[pyo3(get)] + column: usize, +} + +#[pyclass] +struct Message { + #[pyo3(get)] + code: CheckCode, + #[pyo3(get)] + message: String, + #[pyo3(get)] + location: Location, + #[pyo3(get)] + end_location: Location, + // TODO(rgerecke): Include fix +} + +// Using `#[pyclass]` on the `CheckCode` enum is incompatible with serde, +// because this generates unsafe code. +// TODO(rgerecke): Perhaps we want to generate module-level constants instead? +impl IntoPy for CheckCode { + fn into_py(self, py: Python<'_>) -> PyObject { + PyString::new(py, self.as_ref()).into() + } +} + +fn inner_check

(contents: &str, path: P) -> Result> +where + P: AsRef, +{ + // let configuration = Configuration::from_options(options, path.as_ref())?; + let configuration = Configuration::default(); + let settings = Settings::from_configuration(configuration, path.as_ref())?; + + // Tokenize once. + let tokens: Vec = tokenize(contents); + + // Map row and column locations to byte slices (lazily). + let locator = SourceCodeLocator::new(contents); + + // Detect the current code style (lazily). + let stylist = SourceCodeStyleDetector::from_contents(contents, &locator); + + // Extract the `# noqa` and `# isort: skip` directives from the source. + let directives = directives::extract_directives(&tokens, &locator, directives::Flags::empty()); + + // Generate checks. + let checks = check_path( + path.as_ref(), + packages::detect_package_root(path.as_ref()), + contents, + tokens, + &locator, + &stylist, + &directives, + &settings, + flags::Autofix::Enabled, + flags::Noqa::Enabled, + )?; + + Ok(checks) +} + +#[pyfunction] +fn check(contents: &str, path: Option<&str>) -> PyResult> { + // TODO(rgerecke): Accept settings + Ok( + inner_check(contents, path.unwrap_or("")).map(|r| { + r.iter() + .map(|check| Message { + code: check.kind.code().clone(), + message: check.kind.body(), + location: Location { + row: check.location.row(), + column: check.location.column(), + }, + end_location: Location { + row: check.end_location.row(), + column: check.end_location.column(), + }, + }) + .collect::>() + })?, + ) +} #[pymodule] -pub fn _ruff(py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn _ruff(_: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(check, m)?)?; + m.add_class::()?; Ok(()) }