branch: elpa/flymake-collection commit 74e2f3fb4b9b4944f1c90377d7d2f16e57d6d5cd Author: dmitrig <34632025+dmit...@users.noreply.github.com> Commit: GitHub <nore...@github.com>
Set the working dir and shadow file flag for mypy (#9) * Run mypy from inferred python project root * Remove leading dot from temp file name This appears to break relative imports in files checked by mypy. Maybe make the temp file pattern configurable per checker? * Use mypy --shadow-file for buffers with a file Fall back to the buffer name when `buffer-file-name' is nil. Necessary for checking temp files since python module names are based on the file name. * Parse mypy diagnostic ids * Make the mypy working dir configurable * Add basic tests for mypy * Add require for project * Move required mypy flags out of custom variable * Fix parsing of mypy error ids with hyphens * Add temp-file-prefix paramter * Update src/checkers/flymake-collection-mypy.el Co-authored-by: dmitrig <34632025+dgarbu...@users.noreply.github.com> Co-authored-by: Mohsin Kaleem <mohk...@kisara.moe> --- src/checkers/flymake-collection-mypy.el | 93 ++++++++++++++++++++++++++++----- src/flymake-collection-define.el | 24 ++++++--- tests/checkers/installers/mypy.bash | 1 + tests/checkers/test-cases/mypy.yml | 47 +++++++++++++++++ 4 files changed, 144 insertions(+), 21 deletions(-) diff --git a/src/checkers/flymake-collection-mypy.el b/src/checkers/flymake-collection-mypy.el index 1fa8f27444..616d5659b1 100644 --- a/src/checkers/flymake-collection-mypy.el +++ b/src/checkers/flymake-collection-mypy.el @@ -26,34 +26,101 @@ ;;; Code: +(require 'project) (require 'flymake) (require 'flymake-collection) (eval-when-compile (require 'flymake-collection-define)) +(defcustom flymake-collection-mypy-args + '("--show-error-codes") + "Command line arguments always passed to `flymake-collection-mypy'." + :type '(repeat (string :tag "Arg")) + :group 'flymake-collection) + +(defcustom flymake-collection-mypy-project-root + '(("mypy.ini" "project.toml" "setup.cfg") project-root default-directory) + "Method to set project root." + :type '(repeat :tag "Run mypy from the first choice that succeeds" + (choice (const :tag "The buffer default-directory" default-directory) + (const :tag "The current project root" project-root) + (directory :tag "A specific directory") + (repeat :tag "The first ancestor directory containing" + :value ("mypy.ini" "pyproject.toml" "setup.cfg") + (string :tag "File name")))) + :group 'flymake-collection + :safe 'listp) + +(defun flymake-collection-mypy--locate-dominating-files (buffer files) + "Find ancestor directory of `BUFFER' containing any of `FILES'." + (let* ((start (if-let ((file (buffer-file-name buffer))) + (file-name-directory file) + (buffer-local-value 'default-directory buffer))) + (regex (mapconcat 'regexp-quote files "\\|")) + (root (locate-dominating-file + start (lambda (dir) (directory-files dir nil regex t))))) + root)) + +(defun flymake-collection-mypy--default-directory (buffer) + "Find a directory from which to run mypy to check `BUFFER'. +Try each method specified in `flymake-collection-mypy-project-root' in +order and returns the first non-nil result." + (cl-dolist (spec flymake-collection-mypy-project-root) + (when-let + ((res (cond + ((eq spec 'default-directory) + (buffer-local-value 'default-directory buffer)) + ((eq spec 'project-root) + (when-let ((proj (project-current))) + (project-root proj))) + ((listp spec) + (flymake-collection-mypy--locate-dominating-files + buffer spec)) + ((stringp spec) spec)))) + (cl-return res)))) + + ;;;###autoload (autoload 'flymake-collection-mypy "flymake-collection-mypy") (flymake-collection-define-rx flymake-collection-mypy "Mypy syntax and type checker. Requires mypy>=0.580. See URL `http://mypy-lang.org/'." :title "mypy" - :pre-let ((mypy-exec (executable-find "mypy"))) - :pre-check (unless mypy-exec - (error "Cannot find mypy executable")) + :pre-let ((mypy-exec (executable-find "mypy")) + (default-directory (flymake-collection-mypy--default-directory + flymake-collection-source))) + :pre-check (progn + (flymake-log :debug "Working dir is %s" default-directory) + (unless mypy-exec + (error "Cannot find mypy executable")) + (unless default-directory + (error "Default dir is nil: check `flymake-collection-mypy-project-root'"))) :write-type 'file :source-inplace t - :command (list mypy-exec - "--show-column-numbers" - "--no-error-summary" - "--no-color-output" - "--show-absolute-path" - "--show-error-codes" - flymake-collection-temp-file) + :command `(,mypy-exec + "--show-column-numbers" + "--show-absolute-path" + "--no-error-summary" + "--no-color-output" + ,@flymake-collection-mypy-args + ,@(if-let ((source-file (buffer-file-name + flymake-collection-source)) + ((file-exists-p source-file))) + (list + "--shadow-file" source-file flymake-collection-temp-file + source-file) + (list flymake-collection-temp-file))) + ;; Temporary file cannot start with a dot for mypy, see + ;; https://github.com/mohkale/flymake-collection/pull/9 + :temp-file-prefix "flymake_mypy_" :regexps - ((error bol (file-name) ":" line ":" column ": error: " (message) eol) - (warning bol (file-name) ":" line ":" column ": warning: " (message) eol) - (note bol (file-name) ":" line ":" column ": note: " (message) eol))) + ((error bol (file-name) ":" line ":" column ": error: " + (message) (opt " [" (id (* graph)) "]") eol) + (warning bol (file-name) ":" line ":" column ": warning: " + (message) (opt " [" (id (* graph)) "]") eol) + (note bol (file-name) ":" line ":" column ": note: " + (message) (opt " [" (id (* graph)) "]") eol))) (provide 'flymake-collection-mypy) diff --git a/src/flymake-collection-define.el b/src/flymake-collection-define.el index fe7c706e67..7c32039034 100644 --- a/src/flymake-collection-define.el +++ b/src/flymake-collection-define.el @@ -60,11 +60,13 @@ killed and replaced with the new check.") (define-obsolete-function-alias 'flymake-rest-define 'flymake-collection-define "2.0.0") -(defun flymake-collection-define--temp-file (temp-dir temp-file source-inplace) +(defun flymake-collection-define--temp-file + (temp-dir temp-file source-inplace temp-file-prefix) "Let forms for defining a temporary directory and file. TEMP-DIR and TEMP-FILE are the symbols used for the corresponding variables. SOURCE-INPLACE specifies whether the TEMP-DIR should be in the same working -directory as the current buffer." +directory as the current buffer. Temporary files are named by concatenating +TEMP-FILE-PREFIX with the current buffer file name." `((,temp-dir ,@(let ((forms (append @@ -84,7 +86,7 @@ doesn't exist: %s" dir)) (let ((temporary-file-directory ,temp-dir) (basename (file-name-nondirectory (or (buffer-file-name) (buffer-name))))) - (make-temp-file ".flymake_" nil (concat "_" basename)))))) + (make-temp-file ,temp-file-prefix nil (concat "_" basename)))))) (defmacro flymake-collection-define--parse-diags (title proc-symb diags-symb current-diag-symb source-symb error-parser) @@ -132,7 +134,7 @@ CURRENT-DIAGS-SYMB, SOURCE-SYMB, ERROR-PARSER are all described in (cl-defmacro flymake-collection-define (name docstring &optional &key title command error-parser write-type - source-inplace pre-let pre-check) + source-inplace pre-let pre-check (temp-file-prefix ".flymake_")) "Quickly define a backend function for use with Flymake. Define a function NAME which is suitable for use with the variable `flymake-diagnostic-functions'. DOCSTRING if given will become the @@ -192,7 +194,11 @@ used to start the checker process. It should be suitable for use as the ERROR-PARSER is a lisp-form that should, each time it is evaluated, return the next diagnostic from the checker output. The result should be a value that can be passed to the `flymake-make-diagnostic' function. Once -there are no more diagnostics to parse this form should evaluate to nil." +there are no more diagnostics to parse this form should evaluate to nil. + +TEMP-FILE-PREFIX overrides the prefix of temporary file names created by +the checker. This is useful for checker programs that have issues running +on hidden files." (declare (indent defun) (doc-string 2)) (unless lexical-binding (error "Need lexical-binding for flymake-collection-define (%s)" name)) @@ -228,7 +234,7 @@ there are no more diagnostics to parse this form should evaluate to nil." (let* ((,source-symb (current-buffer)) ,@(when (eq write-type 'file) (flymake-collection-define--temp-file - temp-dir-symb temp-file-symb source-inplace)) + temp-dir-symb temp-file-symb source-inplace temp-file-prefix)) ,@pre-let) ;; With vars defined, do pre-check. ,@(when pre-check @@ -441,13 +447,14 @@ For an example of this macro in action, see `flymake-collection-pycodestyle'." (cl-defmacro flymake-collection-define-rx (name docstring - &optional &key title command write-type source-inplace pre-let pre-check regexps) + &optional &key title command write-type + source-inplace pre-let pre-check temp-file-prefix regexps) "`flymake-collection-define' helper using `rx' syntax to parse diagnostics. This helper macro adapts `flymake-collection-define' to use an error-parser built from a collections of REGEXPS (see `flymake-collection-define--parse-rx'). See `flymake-collection-define' for a description of NAME, DOCSTRING, TITLE, -COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, and PRE-CHECK." +COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, PRE-CHECK, and TEMP-FILE-PREFIX." (declare (indent defun) (doc-string 2)) `(flymake-collection-define ,name ,docstring @@ -457,6 +464,7 @@ COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, and PRE-CHECK." :source-inplace ,source-inplace :pre-let ,pre-let :pre-check ,pre-check + :temp-file-prefix ,temp-file-prefix :error-parser (flymake-collection-define--parse-rx ,regexps))) diff --git a/tests/checkers/installers/mypy.bash b/tests/checkers/installers/mypy.bash new file mode 100755 index 0000000000..507e8e903a --- /dev/null +++ b/tests/checkers/installers/mypy.bash @@ -0,0 +1 @@ +python3.8 -m pip install mypy diff --git a/tests/checkers/test-cases/mypy.yml b/tests/checkers/test-cases/mypy.yml new file mode 100644 index 0000000000..6f3fe27229 --- /dev/null +++ b/tests/checkers/test-cases/mypy.yml @@ -0,0 +1,47 @@ +--- +checker: flymake-collection-mypy +tests: + - name: no-lints + file: | + """A test case with no output from mypy.""" + + print("hello world") + lints: [] + - name: error-operator + file: | + """Test parsing an error.""" + + "x" + 1 + lints: + - point: [3, 6] + level: error + message: operator Unsupported operand types for + ("str" and "int") (mypy) + - name: error-attr-defined + file: | + """Test parsing an error with a hyphen in the id.""" + + x = 1 + x.foo + lints: + - point: [4, 0] + level: error + message: attr-defined "int" has no attribute "foo" (mypy) + - name: note-reveal + file: | + """Test parsing a note.""" + + foo = "bar" + reveal_type(foo) + lints: + - point: [4, 12] + level: note + message: Revealed type is "builtins.str" (mypy) + - name: error-syntax + file: | + """Test syntax error.""" + + class Foo: + lints: + - point: [3, 0] + level: error + message: syntax unexpected EOF while parsing (mypy)