Skip to content

Commit

Permalink
Use cargo to find the build target
Browse files Browse the repository at this point in the history
As flycheck/flycheck#1206 has added support for the full conventional project
layout, flycheck-rust should be able to automatically set the variables for
all files in the project as well.

This commit uses `cargo --read-manifest` to get a list of build targets for the
project, and try to determine the correct values for the -rust-crate-type and
-rust-binary-name variables.

As Cargo doesn't actually read the files to determine the targets, we have no
way to know if a file will be compiled, but if the project follows the
conventional layout, we should get it right most of the time.  If not, users can
always override the variables using local variables.

This commit adds test to make sure the association of files to targets works on
a dummy project.
  • Loading branch information
fmdkdd committed Feb 9, 2017
1 parent f8ae845 commit 9f1a4a3
Show file tree
Hide file tree
Showing 20 changed files with 219 additions and 104 deletions.
5 changes: 5 additions & 0 deletions Cask
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
(source melpa)

(package-file "flycheck-rust.el")

(development
(depends-on "buttercup") ; BDD test framework for Emacs
(depends-on "rust-mode")
)
25 changes: 25 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright (c) 2016 fmdkdd

# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.

# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.

# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.

CASK = cask

.PHONY: init
init:
$(CASK) install
$(CASK) update

.PHONY: test
test:
$(CASK) exec buttercup -L .
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ Usage

Just use Flycheck as usual in your Rust/Cargo projects.

**Note:** You must run `cargo build` initially to install all dependencies. If
you add new dependencies to `Cargo.toml` you need to run `cargo build` again.
Otherwise you will see spurious errors about missing crates.

License
-------

Expand Down
176 changes: 76 additions & 100 deletions flycheck-rust.el
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,6 @@
;; # Usage
;;
;; Just use Flycheck as usual in your Rust/Cargo projects.
;;
;; Note: You must run `cargo build` initially to install all dependencies. If
;; you add new dependencies to `Cargo.toml` you need to run `cargo build`
;; again. Otherwise you will see spurious errors about missing crates.

;;; Code:

Expand All @@ -47,86 +43,93 @@
(require 'seq)
(require 'json)

(defun flycheck-rust-executable-p (rel-name)
"Whether REL-NAME denotes an executable.
REL-NAME is the file relative to the Cargo.toml file."
(or (string= "src/main.rs" rel-name)
(string-prefix-p "src/bin/" rel-name)))

(defun flycheck-rust-test-p (rel-name)
"Whether REL-NAME denotes a test.
REL-NAME is the file relative to the Cargo.toml file."
(string-prefix-p "tests/" rel-name))
(defun flycheck-rust-find-manifest (file-name)
"Get the Cargo.toml manifest for FILE-NAME.
(defun flycheck-rust-bench-p (rel-name)
"Whether REL-NAME denotes a bench.
FILE-NAME is the path of a file in a cargo project given as a
string.
REL-NAME is the file relative to the Cargo.toml file."
(string-prefix-p "benches/" rel-name))
See http://doc.crates.io/guide.html for an introduction to the
Cargo.toml manifest.
(defun flycheck-rust-example-p (rel-name)
"Whether REL-NAME denotes an example.
Return the path to the Cargo.toml manifest file, or nil if the
manifest could not be located."
(-when-let (root-dir (locate-dominating-file file-name "Cargo.toml"))
(expand-file-name "Cargo.toml" root-dir)))

REL-NAME is the file relative to the Cargo.toml file."
(string-prefix-p "examples/" rel-name))
(defun flycheck-rust-dirs-list (start end)
"Return a list of directories from START (inclusive) to END (exclusive).
(defun flycheck-rust-project-root ()
"Get the project root for the current buffer.
E.g., if START is '/a/b/c/d' and END is '/a', return the list
'(/a/b/c/d /a/b/c /a/b) in this order.
Return the directory containing the Cargo file, or nil if there
is none."
(locate-dominating-file (buffer-file-name) "Cargo.toml"))
START and END are strings representing file paths. END should be
above START in the file hierarchy; if not, the list stops at the
root of the file hierarchy."
(let ((dirlist)
(dir (expand-file-name start))
(end (expand-file-name end)))
(while (not (or (equal dir (car dirlist)) ; avoid infinite loop
(file-equal-p dir end)))
(push dir dirlist)
(setq dir (directory-file-name (file-name-directory dir))))
(nreverse dirlist)))

(defun flycheck-rust-find-crate-root ()
"Get the crate root (the nearest lib.rs or main.rs)
relative to the current file."
(-if-let (lib-crate-dir (locate-dominating-file (buffer-file-name) "lib.rs"))
(expand-file-name "lib.rs" lib-crate-dir)
(-when-let (exe-crate-dir (locate-dominating-file (buffer-file-name) "main.rs"))
(expand-file-name "main.rs" exe-crate-dir))))

(defun flycheck-rust-binary-crate-p (project-root)
"Determine whether PROJECT-ROOT is a binary crate.
(defun flycheck-rust-find-target (file-name)
"Return the cargo build target associated with the given file.
PROJECT-ROOT is the path to the root directory of the project.
FILE-NAME is the path of the file that is matched against the
`src_path' value in the list of `targets' returned by `cargo
read-manifest'.
Return non-nil if PROJECT-ROOT is a binary crate, nil otherwise."
(let ((root-dir (file-name-directory project-root)))
(file-exists-p (expand-file-name "src/main.rs" root-dir))))
Return a cons cell (KIND . NAME) where KIND is the target
kind (lib, bin, test, example or bench), and NAME the target
name (usually, the crate name). If FILE-NAME exactly matches a
target `src-path', this target is returned. Otherwise, return
the closest matching target, or nil if no targets could be found.
(defun flycheck-rust-find-target (file-name)
"Find and return the cargo target associated with the given file.
FILE-NAME is the name of the file that is matched against the
`src_path' value in the list `targets' returned by `cargo
read-manifest'. If there is no match, the first target is
returned by default.
Return a cons cell (TYPE . NAME), where TYPE is the target
type (lib or bin), and NAME the target name (usually, the crate
name)."
(let ((json-array-type 'list)
(cargo (funcall flycheck-executable-find "cargo")))
See http://doc.crates.io/manifest.html#the-project-layout for a
description of the conventional cargo project layout."
(let ((cargo (funcall flycheck-executable-find "cargo")))
(unless cargo
(user-error "flycheck-rust cannot find `cargo'. Please \
make sure that cargo is installed and on your PATH. See \
http://www.flycheck.org/en/latest/user/troubleshooting.html for \
more information on setting your PATH with Emacs."))
(-let [(&alist 'targets targets)
(with-temp-buffer
(call-process cargo nil t nil "read-manifest")
(goto-char (point-min))
(json-read))]
;; If there is a target that matches the file-name exactly, pick that
;; one. Otherwise, just pick the first target.
(-let [(&alist 'kind (kind) 'name name)
(seq-find (lambda (target)
(-let [(&alist 'src_path src_path) target]
(string= file-name src_path)))
targets (car targets))]
(cons kind name)))))
(-when-let* ((manifest (flycheck-rust-find-manifest file-name))
(targets (let-alist
(with-temp-buffer
(call-process cargo nil t nil "read-manifest"
"--manifest-path" manifest)
(goto-char (point-min))
(let ((json-array-type 'list))
(json-read)))
.targets)))
;; We have targets to work with
(let ((target
(or
;; If there is a target that matches the file-name exactly, pick
;; that one
(seq-find (lambda (target)
(let-alist target (string= file-name .src_path)))
targets)
;; Otherwise find the closest matching target by walking up the tree
;; from FILE-NAME and looking for targets in each directory. E.g.,
;; the file 'tests/common/a.rs' will look for a target in
;; 'tests/common', then in 'tests/', etc.
(car (seq-find
(lambda (pair)
(-let [((&alist 'src_path target-path) . dir) pair]
(file-equal-p dir (file-name-directory target-path))))
;; build a list of (target . dir) candidates
(-table-flat
'cons targets
(flycheck-rust-dirs-list file-name
(file-name-directory manifest)))))
;; If all else fails, just pick the first target
(car targets))))
;; Return the cons cell
(let-alist target (cons (car .kind) .name))))))

;;;###autoload
(defun flycheck-rust-setup ()
Expand All @@ -139,37 +142,10 @@ Flycheck according to the Cargo project layout."
;; with `global-flycheck-mode' it will render Emacs unusable (see
;; https://github.com/flycheck/flycheck-rust/issues/40#issuecomment-253760883).
(with-demoted-errors "Error in flycheck-rust-setup: %S"
(when (buffer-file-name)
(-when-let (root (flycheck-rust-project-root))
(pcase-let ((rel-name (file-relative-name (buffer-file-name) root))
(`(,target-type . ,target-name) (flycheck-rust-find-target
(buffer-file-name))))
;; These are valid crate roots as by Cargo's layout
(if (or (flycheck-rust-executable-p rel-name)
(flycheck-rust-test-p rel-name)
(flycheck-rust-bench-p rel-name)
(flycheck-rust-example-p rel-name)
(string= "src/lib.rs" rel-name))
(setq-local flycheck-rust-crate-root rel-name)
;; For other files, the library is either the default library or the
;; executable
(setq-local flycheck-rust-crate-root (flycheck-rust-find-crate-root)))
;; Check tests in libraries and integration tests
(setq-local flycheck-rust-check-tests
(not (flycheck-rust-executable-p rel-name)))
;; Set the crate type
(setq-local flycheck-rust-crate-type
(if (string= target-type "bin")
(progn
;; If it's binary target, we need to pass the binary
;; name
(setq-local flycheck-rust-binary-name target-name)
"bin")
"lib"))
;; Find build libraries
(setq-local flycheck-rust-library-path
(list (expand-file-name "target/debug" root)
(expand-file-name "target/debug/deps" root))))))))
(-when-let* ((file-name (buffer-file-name))
((kind . name) (flycheck-rust-find-target file-name)))
(setq-local flycheck-rust-crate-type kind)
(setq-local flycheck-rust-binary-name name))))

(provide 'flycheck-rust)

Expand Down
3 changes: 3 additions & 0 deletions tests/test-crate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[package]
name = "test-crate"
version = "0.1.0"
Empty file added tests/test-crate/benches/a.rs
Empty file.
Empty file added tests/test-crate/benches/b.rs
Empty file.
Empty file.
Empty file added tests/test-crate/examples/a.rs
Empty file.
Empty file added tests/test-crate/examples/b.rs
Empty file.
Empty file.
Empty file added tests/test-crate/src/a.rs
Empty file.
Empty file added tests/test-crate/src/bin/a.rs
Empty file.
Empty file added tests/test-crate/src/bin/b.rs
Empty file.
Empty file added tests/test-crate/src/lib.rs
Empty file.
Empty file added tests/test-crate/src/main.rs
Empty file.
Empty file added tests/test-crate/tests/a.rs
Empty file.
Empty file added tests/test-crate/tests/b.rs
Empty file.
Empty file.
110 changes: 110 additions & 0 deletions tests/test-rust-setup.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
;;; test-rust-setup.el -*- lexical-binding: t; -*-

;; Copyright (C) 2016 fmdkdd

;; Author: fmdkdd

;; This file is not part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.

;;; Commentary:

;; Integration tests for `flycheck-rust-setup'.

;;; Code:

(require 'flycheck-rust)

(buttercup-define-matcher :to-equal-one-of (elt &rest seq)
(if (member elt seq)
(cons t (format "Expected %S not to equal a member of %S" elt seq))
(cons nil (format "Expected %S to equal a member of %S" elt seq))))

(defun crate-file (file-name)
(expand-file-name file-name "tests/test-crate"))

(describe
"`flycheck-rust-find-target' associates"

(it "'src/lib.rs' to the library target"
(expect
(car (flycheck-rust-find-target (crate-file "src/lib.rs")))
:to-equal "lib"))

(it "'src/a.rs' to the library target"
(expect
(car (flycheck-rust-find-target (crate-file "src/a.rs")))
:to-equal "lib"))

(it "'src/main.rs' to the main binary target"
(expect
(flycheck-rust-find-target (crate-file "src/main.rs"))
:to-equal '("bin" . "test-crate")))

(it "'src/bin/a.rs' to the 'a' binary target"
(expect
(flycheck-rust-find-target (crate-file "src/bin/a.rs"))
:to-equal '("bin" . "a")))

(it "'src/bin/b.rs' to the 'b' binary target"
(expect
(flycheck-rust-find-target (crate-file "src/bin/b.rs"))
:to-equal '("bin" . "b")))

(it "'src/bin/support/mod.rs' to the 'b' binary target"
(expect
(flycheck-rust-find-target (crate-file "src/bin/support/mod.rs"))
:to-equal '("bin" . "b")))

(it "'tests/a.rs' to the 'a' test target"
(expect
(flycheck-rust-find-target (crate-file "tests/a.rs"))
:to-equal '("test" . "a")))

(it "'tests/support/mod.rs' to any test target"
(expect
(flycheck-rust-find-target (crate-file "tests/support/mod.rs"))
:to-equal-one-of '("test". "a") '("test". "b")))

(it "'examples/a.rs' to the 'a' example target"
(expect
(flycheck-rust-find-target (crate-file "examples/a.rs"))
:to-equal '("example" . "a")))

(it "'examples/b.rs' to the 'b' example target"
(expect
(flycheck-rust-find-target (crate-file "examples/b.rs"))
:to-equal '("example" . "b")))

(it "'examples/support/mod.rs' to any example target"
(expect
(flycheck-rust-find-target (crate-file "examples/support/mod.rs"))
:to-equal-one-of '("example" . "a") '("example" . "b")))

(it "'benches/a.rs' to the 'a' bench target"
(expect
(flycheck-rust-find-target (crate-file "benches/a.rs"))
:to-equal '("bench" . "a")))

(it "'benches/b.rs' to the 'b' bench target"
(expect
(flycheck-rust-find-target (crate-file "benches/b.rs"))
:to-equal '("bench" . "b")))

(it "'benches/support/mod.rs' to any bench target"
(expect
(flycheck-rust-find-target (crate-file "benches/support/mod.rs"))
:to-equal-one-of '("bench" . "a") '("bench" . "b")))
)

0 comments on commit 9f1a4a3

Please sign in to comment.