Skip to content

Commit d5affd9

Browse files
committed
Use cargo to find the build target
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.
1 parent f8ae845 commit d5affd9

File tree

20 files changed

+234
-105
lines changed

20 files changed

+234
-105
lines changed

Cask

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
(source melpa)
33

44
(package-file "flycheck-rust.el")
5+
6+
(development
7+
(depends-on "buttercup") ; BDD test framework for Emacs
8+
(depends-on "rust-mode")
9+
)

Makefile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Copyright (c) 2016 fmdkdd
2+
3+
# This program is free software: you can redistribute it and/or modify it under
4+
# the terms of the GNU General Public License as published by the Free Software
5+
# Foundation, either version 3 of the License, or (at your option) any later
6+
# version.
7+
8+
# This program is distributed in the hope that it will be useful, but WITHOUT
9+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
10+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
11+
# details.
12+
13+
# You should have received a copy of the GNU General Public License along with
14+
# this program. If not, see <http://www.gnu.org/licenses/>.
15+
16+
CASK = cask
17+
18+
.PHONY: init
19+
init:
20+
$(CASK) install
21+
$(CASK) update
22+
23+
.PHONY: test
24+
test:
25+
$(CASK) exec buttercup -L .

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,6 @@ Usage
3232

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

35-
**Note:** You must run `cargo build` initially to install all dependencies. If
36-
you add new dependencies to `Cargo.toml` you need to run `cargo build` again.
37-
Otherwise you will see spurious errors about missing crates.
38-
3935
License
4036
-------
4137

flycheck-rust.el

Lines changed: 91 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@
3535
;; # Usage
3636
;;
3737
;; Just use Flycheck as usual in your Rust/Cargo projects.
38-
;;
39-
;; Note: You must run `cargo build` initially to install all dependencies. If
40-
;; you add new dependencies to `Cargo.toml` you need to run `cargo build`
41-
;; again. Otherwise you will see spurious errors about missing crates.
4238

4339
;;; Code:
4440

@@ -47,86 +43,107 @@
4743
(require 'seq)
4844
(require 'json)
4945

50-
(defun flycheck-rust-executable-p (rel-name)
51-
"Whether REL-NAME denotes an executable.
52-
53-
REL-NAME is the file relative to the Cargo.toml file."
54-
(or (string= "src/main.rs" rel-name)
55-
(string-prefix-p "src/bin/" rel-name)))
56-
57-
(defun flycheck-rust-test-p (rel-name)
58-
"Whether REL-NAME denotes a test.
59-
60-
REL-NAME is the file relative to the Cargo.toml file."
61-
(string-prefix-p "tests/" rel-name))
62-
63-
(defun flycheck-rust-bench-p (rel-name)
64-
"Whether REL-NAME denotes a bench.
65-
66-
REL-NAME is the file relative to the Cargo.toml file."
67-
(string-prefix-p "benches/" rel-name))
68-
69-
(defun flycheck-rust-example-p (rel-name)
70-
"Whether REL-NAME denotes an example.
71-
72-
REL-NAME is the file relative to the Cargo.toml file."
73-
(string-prefix-p "examples/" rel-name))
46+
(defun flycheck-rust-find-manifest (file-name)
47+
"Get the Cargo.toml manifest for FILE-NAME.
7448
75-
(defun flycheck-rust-project-root ()
76-
"Get the project root for the current buffer.
49+
FILE-NAME is the path of a file in a cargo project given as a
50+
string.
7751
78-
Return the directory containing the Cargo file, or nil if there
79-
is none."
80-
(locate-dominating-file (buffer-file-name) "Cargo.toml"))
52+
See http://doc.crates.io/guide.html for an introduction to the
53+
Cargo.toml manifest.
8154
82-
(defun flycheck-rust-find-crate-root ()
83-
"Get the crate root (the nearest lib.rs or main.rs)
84-
relative to the current file."
85-
(-if-let (lib-crate-dir (locate-dominating-file (buffer-file-name) "lib.rs"))
86-
(expand-file-name "lib.rs" lib-crate-dir)
87-
(-when-let (exe-crate-dir (locate-dominating-file (buffer-file-name) "main.rs"))
88-
(expand-file-name "main.rs" exe-crate-dir))))
55+
Return the path to the Cargo.toml manifest file, or nil if the
56+
manifest could not be located."
57+
(-when-let (root-dir (locate-dominating-file file-name "Cargo.toml"))
58+
(expand-file-name "Cargo.toml" root-dir)))
8959

90-
(defun flycheck-rust-binary-crate-p (project-root)
91-
"Determine whether PROJECT-ROOT is a binary crate.
60+
(defun flycheck-rust-dirs-list (start end)
61+
"Return a list of directories from START (inclusive) to END (exclusive).
9262
93-
PROJECT-ROOT is the path to the root directory of the project.
63+
E.g., if START is '/a/b/c/d' and END is '/a', return the list
64+
'(/a/b/c/d /a/b/c /a/b) in this order.
9465
95-
Return non-nil if PROJECT-ROOT is a binary crate, nil otherwise."
96-
(let ((root-dir (file-name-directory project-root)))
97-
(file-exists-p (expand-file-name "src/main.rs" root-dir))))
66+
START and END are strings representing file paths. END should be
67+
above START in the file hierarchy; if not, the list stops at the
68+
root of the file hierarchy."
69+
(let ((dirlist)
70+
(dir (expand-file-name start))
71+
(end (expand-file-name end)))
72+
(while (not (or (equal dir (car dirlist)) ; avoid infinite loop
73+
(file-equal-p dir end)))
74+
(push dir dirlist)
75+
(setq dir (directory-file-name (file-name-directory dir))))
76+
(nreverse dirlist)))
9877

99-
(defun flycheck-rust-find-target (file-name)
100-
"Find and return the cargo target associated with the given file.
78+
(defun flycheck-rust-get-cargo-targets (manifest)
79+
"Return the list of available Cargo targets for the given project.
10180
102-
FILE-NAME is the name of the file that is matched against the
103-
`src_path' value in the list `targets' returned by `cargo
104-
read-manifest'. If there is no match, the first target is
105-
returned by default.
81+
MANIFEST is the path to the Cargo.toml file of the project.
10682
107-
Return a cons cell (TYPE . NAME), where TYPE is the target
108-
type (lib or bin), and NAME the target name (usually, the crate
109-
name)."
110-
(let ((json-array-type 'list)
111-
(cargo (funcall flycheck-executable-find "cargo")))
83+
Calls `cargo metadata --no-deps --manifest MANIFEST', parses and
84+
collects the targets for the current workspace, and returns them
85+
in a list, or nil if no targets could be found."
86+
(let ((cargo (funcall flycheck-executable-find "cargo")))
11287
(unless cargo
11388
(user-error "flycheck-rust cannot find `cargo'. Please \
11489
make sure that cargo is installed and on your PATH. See \
11590
http://www.flycheck.org/en/latest/user/troubleshooting.html for \
11691
more information on setting your PATH with Emacs."))
117-
(-let [(&alist 'targets targets)
118-
(with-temp-buffer
119-
(call-process cargo nil t nil "read-manifest")
120-
(goto-char (point-min))
121-
(json-read))]
122-
;; If there is a target that matches the file-name exactly, pick that
123-
;; one. Otherwise, just pick the first target.
124-
(-let [(&alist 'kind (kind) 'name name)
125-
(seq-find (lambda (target)
126-
(-let [(&alist 'src_path src_path) target]
127-
(string= file-name src_path)))
128-
targets (car targets))]
129-
(cons kind name)))))
92+
;; metadata contains a list of packages, and each package has a list of
93+
;; targets. We concatenate all targets, regardless of the package.
94+
(when-let ((packages (let-alist
95+
(with-temp-buffer
96+
(call-process cargo nil t nil
97+
"metadata" "--no-deps"
98+
"--manifest-path" manifest)
99+
(goto-char (point-min))
100+
(let ((json-array-type 'list))
101+
(json-read)))
102+
.packages)))
103+
(seq-mapcat (lambda (pkg)
104+
(let-alist pkg .targets))
105+
packages))))
106+
107+
(defun flycheck-rust-find-cargo-target (file-name)
108+
"Return the Cargo build target associated with FILE-NAME.
109+
110+
FILE-NAME is the path of the file that is matched against the
111+
`src_path' value in the list of `targets' returned by `cargo
112+
read-manifest'.
113+
114+
Return a cons cell (KIND . NAME) where KIND is the target
115+
kind (lib, bin, test, example or bench), and NAME the target
116+
name (usually, the crate name). If FILE-NAME exactly matches a
117+
target `src-path', this target is returned. Otherwise, return
118+
the closest matching target, or nil if no targets could be found.
119+
120+
See http://doc.crates.io/manifest.html#the-project-layout for a
121+
description of the conventional Cargo project layout."
122+
(-when-let* ((manifest (flycheck-rust-find-manifest file-name))
123+
(targets (flycheck-rust-get-cargo-targets manifest)))
124+
(let ((target
125+
(or
126+
;; If there is a target that matches the file-name exactly, pick
127+
;; that one
128+
(seq-find (lambda (target)
129+
(let-alist target (string= file-name .src_path)))
130+
targets)
131+
;; Otherwise find the closest matching target by walking up the tree
132+
;; from FILE-NAME and looking for targets in each directory. E.g.,
133+
;; the file 'tests/common/a.rs' will look for a target in
134+
;; 'tests/common', then in 'tests/', etc.
135+
(car (seq-find
136+
(lambda (pair)
137+
(-let [((&alist 'src_path target-path) . dir) pair]
138+
(file-equal-p dir (file-name-directory target-path))))
139+
;; build a list of (target . dir) candidates
140+
(-table-flat
141+
'cons targets
142+
(flycheck-rust-dirs-list file-name
143+
(file-name-directory manifest)))))
144+
;; If all else fails, just pick the first target
145+
(car targets))))
146+
(let-alist target (cons (car .kind) .name)))))
130147

131148
;;;###autoload
132149
(defun flycheck-rust-setup ()
@@ -139,37 +156,10 @@ Flycheck according to the Cargo project layout."
139156
;; with `global-flycheck-mode' it will render Emacs unusable (see
140157
;; https://github.com/flycheck/flycheck-rust/issues/40#issuecomment-253760883).
141158
(with-demoted-errors "Error in flycheck-rust-setup: %S"
142-
(when (buffer-file-name)
143-
(-when-let (root (flycheck-rust-project-root))
144-
(pcase-let ((rel-name (file-relative-name (buffer-file-name) root))
145-
(`(,target-type . ,target-name) (flycheck-rust-find-target
146-
(buffer-file-name))))
147-
;; These are valid crate roots as by Cargo's layout
148-
(if (or (flycheck-rust-executable-p rel-name)
149-
(flycheck-rust-test-p rel-name)
150-
(flycheck-rust-bench-p rel-name)
151-
(flycheck-rust-example-p rel-name)
152-
(string= "src/lib.rs" rel-name))
153-
(setq-local flycheck-rust-crate-root rel-name)
154-
;; For other files, the library is either the default library or the
155-
;; executable
156-
(setq-local flycheck-rust-crate-root (flycheck-rust-find-crate-root)))
157-
;; Check tests in libraries and integration tests
158-
(setq-local flycheck-rust-check-tests
159-
(not (flycheck-rust-executable-p rel-name)))
160-
;; Set the crate type
161-
(setq-local flycheck-rust-crate-type
162-
(if (string= target-type "bin")
163-
(progn
164-
;; If it's binary target, we need to pass the binary
165-
;; name
166-
(setq-local flycheck-rust-binary-name target-name)
167-
"bin")
168-
"lib"))
169-
;; Find build libraries
170-
(setq-local flycheck-rust-library-path
171-
(list (expand-file-name "target/debug" root)
172-
(expand-file-name "target/debug/deps" root))))))))
159+
(-when-let* ((file-name (buffer-file-name))
160+
((kind . name) (flycheck-rust-find-cargo-target file-name)))
161+
(setq-local flycheck-rust-crate-type kind)
162+
(setq-local flycheck-rust-binary-name name))))
173163

174164
(provide 'flycheck-rust)
175165

tests/test-crate/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[package]
2+
name = "test-crate"
3+
version = "0.1.0"

tests/test-crate/benches/a.rs

Whitespace-only changes.

tests/test-crate/benches/b.rs

Whitespace-only changes.

tests/test-crate/benches/support/mod.rs

Whitespace-only changes.

tests/test-crate/examples/a.rs

Whitespace-only changes.

tests/test-crate/examples/b.rs

Whitespace-only changes.

tests/test-crate/examples/support/mod.rs

Whitespace-only changes.

tests/test-crate/src/a.rs

Whitespace-only changes.

tests/test-crate/src/bin/a.rs

Whitespace-only changes.

tests/test-crate/src/bin/b.rs

Whitespace-only changes.

tests/test-crate/src/lib.rs

Whitespace-only changes.

tests/test-crate/src/main.rs

Whitespace-only changes.

tests/test-crate/tests/a.rs

Whitespace-only changes.

tests/test-crate/tests/b.rs

Whitespace-only changes.

tests/test-crate/tests/support/mod.rs

Whitespace-only changes.

tests/test-rust-setup.el

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
;;; test-rust-setup.el -*- lexical-binding: t; -*-
2+
3+
;; Copyright (C) 2016 fmdkdd
4+
5+
;; Author: fmdkdd
6+
7+
;; This file is not part of GNU Emacs.
8+
9+
;; This program is free software; you can redistribute it and/or modify
10+
;; it under the terms of the GNU General Public License as published by
11+
;; the Free Software Foundation, either version 3 of the License, or
12+
;; (at your option) any later version.
13+
14+
;; This program is distributed in the hope that it will be useful,
15+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
;; GNU General Public License for more details.
18+
19+
;; You should have received a copy of the GNU General Public License
20+
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
22+
;;; Commentary:
23+
24+
;; Integration tests for `flycheck-rust-setup'.
25+
26+
;;; Code:
27+
28+
(require 'flycheck-rust)
29+
30+
(buttercup-define-matcher :to-equal-one-of (elt &rest seq)
31+
(if (member elt seq)
32+
(cons t (format "Expected %S not to equal a member of %S" elt seq))
33+
(cons nil (format "Expected %S to equal a member of %S" elt seq))))
34+
35+
(defun crate-file (file-name)
36+
(expand-file-name file-name "tests/test-crate"))
37+
38+
(describe
39+
"`flycheck-rust-find-cargo-target' associates"
40+
41+
(it "'src/lib.rs' to the library target"
42+
(expect
43+
(car (flycheck-rust-find-cargo-target (crate-file "src/lib.rs")))
44+
:to-equal "lib"))
45+
46+
(it "'src/a.rs' to the library target"
47+
(expect
48+
(car (flycheck-rust-find-cargo-target (crate-file "src/a.rs")))
49+
:to-equal "lib"))
50+
51+
(it "'src/main.rs' to the main binary target"
52+
(expect
53+
(flycheck-rust-find-cargo-target (crate-file "src/main.rs"))
54+
:to-equal '("bin" . "test-crate")))
55+
56+
(it "'src/bin/a.rs' to the 'a' binary target"
57+
(expect
58+
(flycheck-rust-find-cargo-target (crate-file "src/bin/a.rs"))
59+
:to-equal '("bin" . "a")))
60+
61+
(it "'src/bin/b.rs' to the 'b' binary target"
62+
(expect
63+
(flycheck-rust-find-cargo-target (crate-file "src/bin/b.rs"))
64+
:to-equal '("bin" . "b")))
65+
66+
(it "'src/bin/support/mod.rs' to the 'b' binary target"
67+
(expect
68+
(flycheck-rust-find-cargo-target (crate-file "src/bin/support/mod.rs"))
69+
:to-equal '("bin" . "b")))
70+
71+
(it "'tests/a.rs' to the 'a' test target"
72+
(expect
73+
(flycheck-rust-find-cargo-target (crate-file "tests/a.rs"))
74+
:to-equal '("test" . "a")))
75+
76+
(it "'tests/support/mod.rs' to any test target"
77+
(expect
78+
(flycheck-rust-find-cargo-target (crate-file "tests/support/mod.rs"))
79+
:to-equal-one-of '("test". "a") '("test". "b")))
80+
81+
(it "'examples/a.rs' to the 'a' example target"
82+
(expect
83+
(flycheck-rust-find-cargo-target (crate-file "examples/a.rs"))
84+
:to-equal '("example" . "a")))
85+
86+
(it "'examples/b.rs' to the 'b' example target"
87+
(expect
88+
(flycheck-rust-find-cargo-target (crate-file "examples/b.rs"))
89+
:to-equal '("example" . "b")))
90+
91+
(it "'examples/support/mod.rs' to any example target"
92+
(expect
93+
(flycheck-rust-find-cargo-target (crate-file "examples/support/mod.rs"))
94+
:to-equal-one-of '("example" . "a") '("example" . "b")))
95+
96+
(it "'benches/a.rs' to the 'a' bench target"
97+
(expect
98+
(flycheck-rust-find-cargo-target (crate-file "benches/a.rs"))
99+
:to-equal '("bench" . "a")))
100+
101+
(it "'benches/b.rs' to the 'b' bench target"
102+
(expect
103+
(flycheck-rust-find-cargo-target (crate-file "benches/b.rs"))
104+
:to-equal '("bench" . "b")))
105+
106+
(it "'benches/support/mod.rs' to any bench target"
107+
(expect
108+
(flycheck-rust-find-cargo-target (crate-file "benches/support/mod.rs"))
109+
:to-equal-one-of '("bench" . "a") '("bench" . "b")))
110+
)

0 commit comments

Comments
 (0)