aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/release.yaml1
-rw-r--r--Cargo.lock23
-rw-r--r--editors/code/src/commands/server_version.ts2
-rw-r--r--editors/code/src/config.ts41
-rw-r--r--editors/code/src/ctx.ts6
-rw-r--r--editors/code/src/installation/extension.ts16
-rw-r--r--editors/code/src/installation/server.ts21
-rw-r--r--editors/code/src/main.ts10
-rw-r--r--editors/code/src/persistent_state.ts49
-rw-r--r--xtask/tests/tidy-tests/cli.rs25
-rw-r--r--xtask/tests/tidy-tests/docs.rs106
-rw-r--r--xtask/tests/tidy-tests/main.rs145
12 files changed, 233 insertions, 212 deletions
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 0434b6128..21ac3a4bc 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -11,6 +11,7 @@ jobs:
11 dist: 11 dist:
12 name: dist 12 name: dist
13 runs-on: ${{ matrix.os }} 13 runs-on: ${{ matrix.os }}
14 if: github.repository == "rust-analyzer/rust-analyzer"
14 strategy: 15 strategy:
15 matrix: 16 matrix:
16 os: [ubuntu-latest, windows-latest, macos-latest] 17 os: [ubuntu-latest, windows-latest, macos-latest]
diff --git a/Cargo.lock b/Cargo.lock
index efe8dd189..f6df77206 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -419,9 +419,9 @@ dependencies = [
419 419
420[[package]] 420[[package]]
421name = "globset" 421name = "globset"
422version = "0.4.4" 422version = "0.4.5"
423source = "registry+https://github.com/rust-lang/crates.io-index" 423source = "registry+https://github.com/rust-lang/crates.io-index"
424checksum = "925aa2cac82d8834e2b2a4415b6f6879757fb5c0928fc445ae76461a12eed8f2" 424checksum = "7ad1da430bd7281dde2576f44c84cc3f0f7b475e7202cd503042dff01a8c8120"
425dependencies = [ 425dependencies = [
426 "aho-corasick", 426 "aho-corasick",
427 "bstr", 427 "bstr",
@@ -668,11 +668,11 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
668 668
669[[package]] 669[[package]]
670name = "memoffset" 670name = "memoffset"
671version = "0.5.3" 671version = "0.5.4"
672source = "registry+https://github.com/rust-lang/crates.io-index" 672source = "registry+https://github.com/rust-lang/crates.io-index"
673checksum = "75189eb85871ea5c2e2c15abbdd541185f63b408415e5051f5cac122d8c774b9" 673checksum = "b4fc2c02a7e374099d4ee95a193111f72d2110197fe200272371758f6c3643d8"
674dependencies = [ 674dependencies = [
675 "rustc_version", 675 "autocfg",
676] 676]
677 677
678[[package]] 678[[package]]
@@ -1133,9 +1133,9 @@ dependencies = [
1133 1133
1134[[package]] 1134[[package]]
1135name = "ra_vfs" 1135name = "ra_vfs"
1136version = "0.5.2" 1136version = "0.5.3"
1137source = "registry+https://github.com/rust-lang/crates.io-index" 1137source = "registry+https://github.com/rust-lang/crates.io-index"
1138checksum = "bc898f237e4b4498959ae0100c688793a23e77624d44ef710ba70094217f98e0" 1138checksum = "58a265769d5e5655345a9fcbd870a1a7c3658558c0d8efaed79e0669358f46b8"
1139dependencies = [ 1139dependencies = [
1140 "crossbeam-channel", 1140 "crossbeam-channel",
1141 "jod-thread", 1141 "jod-thread",
@@ -1332,15 +1332,6 @@ dependencies = [
1332] 1332]
1333 1333
1334[[package]] 1334[[package]]
1335name = "rustc_version"
1336version = "0.2.3"
1337source = "registry+https://github.com/rust-lang/crates.io-index"
1338checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
1339dependencies = [
1340 "semver",
1341]
1342
1343[[package]]
1344name = "ryu" 1335name = "ryu"
1345version = "1.0.3" 1336version = "1.0.3"
1346source = "registry+https://github.com/rust-lang/crates.io-index" 1337source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/editors/code/src/commands/server_version.ts b/editors/code/src/commands/server_version.ts
index c4d84b443..83b1acf67 100644
--- a/editors/code/src/commands/server_version.ts
+++ b/editors/code/src/commands/server_version.ts
@@ -5,7 +5,7 @@ import { spawnSync } from 'child_process';
5 5
6export function serverVersion(ctx: Ctx): Cmd { 6export function serverVersion(ctx: Ctx): Cmd {
7 return async () => { 7 return async () => {
8 const binaryPath = await ensureServerBinary(ctx.config); 8 const binaryPath = await ensureServerBinary(ctx.config, ctx.state);
9 9
10 if (binaryPath == null) { 10 if (binaryPath == null) {
11 throw new Error( 11 throw new Error(
diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts
index f63e1d20e..bd8096dd6 100644
--- a/editors/code/src/config.ts
+++ b/editors/code/src/config.ts
@@ -182,13 +182,6 @@ export class Config {
182 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG); 182 return this.createGithubReleaseSource("rust-analyzer.vsix", NIGHTLY_TAG);
183 } 183 }
184 184
185 readonly installedNightlyExtensionReleaseDate = new DateStorage(
186 "installed-nightly-extension-release-date",
187 this.ctx.globalState
188 );
189 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
190 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
191
192 // We don't do runtime config validation here for simplicity. More on stackoverflow: 185 // We don't do runtime config validation here for simplicity. More on stackoverflow:
193 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension 186 // https://stackoverflow.com/questions/60135780/what-is-the-best-way-to-type-check-the-configuration-for-vscode-extension
194 187
@@ -232,37 +225,3 @@ export class Config {
232 // for internal use 225 // for internal use
233 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; } 226 get withSysroot() { return this.cfg.get("withSysroot", true) as boolean; }
234} 227}
235
236export class Storage<T> {
237 constructor(
238 private readonly key: string,
239 private readonly storage: vscode.Memento,
240 private readonly defaultVal: T
241 ) { }
242
243 get(): T {
244 const val = this.storage.get(this.key, this.defaultVal);
245 log.debug(this.key, "==", val);
246 return val;
247 }
248 async set(val: T) {
249 log.debug(this.key, "=", val);
250 await this.storage.update(this.key, val);
251 }
252}
253export class DateStorage {
254 inner: Storage<null | string>;
255
256 constructor(key: string, storage: vscode.Memento) {
257 this.inner = new Storage(key, storage, null);
258 }
259
260 get(): null | Date {
261 const dateStr = this.inner.get();
262 return dateStr ? new Date(dateStr) : null;
263 }
264
265 async set(date: null | Date) {
266 await this.inner.set(date ? date.toString() : null);
267 }
268}
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 25ef38aed..c929ab063 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -4,19 +4,21 @@ import * as lc from 'vscode-languageclient';
4import { Config } from './config'; 4import { Config } from './config';
5import { createClient } from './client'; 5import { createClient } from './client';
6import { isRustEditor, RustEditor } from './util'; 6import { isRustEditor, RustEditor } from './util';
7import { PersistentState } from './persistent_state';
7 8
8export class Ctx { 9export class Ctx {
9 private constructor( 10 private constructor(
10 readonly config: Config, 11 readonly config: Config,
12 readonly state: PersistentState,
11 private readonly extCtx: vscode.ExtensionContext, 13 private readonly extCtx: vscode.ExtensionContext,
12 readonly client: lc.LanguageClient 14 readonly client: lc.LanguageClient
13 ) { 15 ) {
14 16
15 } 17 }
16 18
17 static async create(config: Config, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> { 19 static async create(config: Config, state: PersistentState, extCtx: vscode.ExtensionContext, serverPath: string): Promise<Ctx> {
18 const client = await createClient(config, serverPath); 20 const client = await createClient(config, serverPath);
19 const res = new Ctx(config, extCtx, client); 21 const res = new Ctx(config, state, extCtx, client);
20 res.pushCleanup(client.start()); 22 res.pushCleanup(client.start());
21 await client.onReady(); 23 await client.onReady();
22 return res; 24 return res;
diff --git a/editors/code/src/installation/extension.ts b/editors/code/src/installation/extension.ts
index eea6fded2..a1db96f05 100644
--- a/editors/code/src/installation/extension.ts
+++ b/editors/code/src/installation/extension.ts
@@ -7,6 +7,7 @@ import { Config, UpdatesChannel } from "../config";
7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces"; 7import { ArtifactReleaseInfo, ArtifactSource } from "./interfaces";
8import { downloadArtifactWithProgressUi } from "./downloads"; 8import { downloadArtifactWithProgressUi } from "./downloads";
9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info"; 9import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
10import { PersistentState } from "../persistent_state";
10 11
11const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25; 12const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
12 13
@@ -14,7 +15,7 @@ const HEURISTIC_NIGHTLY_RELEASE_PERIOD_IN_HOURS = 25;
14 * Installs `stable` or latest `nightly` version or does nothing if the current 15 * Installs `stable` or latest `nightly` version or does nothing if the current
15 * extension version is what's needed according to `desiredUpdateChannel`. 16 * extension version is what's needed according to `desiredUpdateChannel`.
16 */ 17 */
17export async function ensureProperExtensionVersion(config: Config): Promise<never | void> { 18export async function ensureProperExtensionVersion(config: Config, state: PersistentState): Promise<never | void> {
18 // User has built lsp server from sources, she should manage updates manually 19 // User has built lsp server from sources, she should manage updates manually
19 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return; 20 if (config.serverSource?.type === ArtifactSource.Type.ExplicitPath) return;
20 21
@@ -23,7 +24,7 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
23 24
24 if (currentUpdChannel === UpdatesChannel.Stable) { 25 if (currentUpdChannel === UpdatesChannel.Stable) {
25 // Release date is present only when we are on nightly 26 // Release date is present only when we are on nightly
26 await config.installedNightlyExtensionReleaseDate.set(null); 27 await state.installedNightlyExtensionReleaseDate.set(null);
27 } 28 }
28 29
29 if (desiredUpdChannel === UpdatesChannel.Stable) { 30 if (desiredUpdChannel === UpdatesChannel.Stable) {
@@ -39,10 +40,10 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
39 if (currentUpdChannel === UpdatesChannel.Stable) { 40 if (currentUpdChannel === UpdatesChannel.Stable) {
40 if (!await askToDownloadProperExtensionVersion(config)) return; 41 if (!await askToDownloadProperExtensionVersion(config)) return;
41 42
42 return await tryDownloadNightlyExtension(config); 43 return await tryDownloadNightlyExtension(config, state);
43 } 44 }
44 45
45 const currentExtReleaseDate = config.installedNightlyExtensionReleaseDate.get(); 46 const currentExtReleaseDate = state.installedNightlyExtensionReleaseDate.get();
46 47
47 if (currentExtReleaseDate === null) { 48 if (currentExtReleaseDate === null) {
48 void vscode.window.showErrorMessage( 49 void vscode.window.showErrorMessage(
@@ -66,9 +67,9 @@ export async function ensureProperExtensionVersion(config: Config): Promise<neve
66 return; 67 return;
67 } 68 }
68 69
69 await tryDownloadNightlyExtension(config, releaseInfo => { 70 await tryDownloadNightlyExtension(config, state, releaseInfo => {
70 assert( 71 assert(
71 currentExtReleaseDate.getTime() === config.installedNightlyExtensionReleaseDate.get()?.getTime(), 72 currentExtReleaseDate.getTime() === state.installedNightlyExtensionReleaseDate.get()?.getTime(),
72 "Other active VSCode instance has reinstalled the extension" 73 "Other active VSCode instance has reinstalled the extension"
73 ); 74 );
74 75
@@ -111,6 +112,7 @@ async function askToDownloadProperExtensionVersion(config: Config, reason = "")
111 */ 112 */
112const tryDownloadNightlyExtension = notReentrant(async ( 113const tryDownloadNightlyExtension = notReentrant(async (
113 config: Config, 114 config: Config,
115 state: PersistentState,
114 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true 116 shouldDownload: (releaseInfo: ArtifactReleaseInfo) => boolean = () => true
115): Promise<never | void> => { 117): Promise<never | void> => {
116 const vsixSource = config.nightlyVsixSource; 118 const vsixSource = config.nightlyVsixSource;
@@ -124,7 +126,7 @@ const tryDownloadNightlyExtension = notReentrant(async (
124 const vsixPath = path.join(vsixSource.dir, vsixSource.file); 126 const vsixPath = path.join(vsixSource.dir, vsixSource.file);
125 127
126 await vscodeInstallExtensionFromVsix(vsixPath); 128 await vscodeInstallExtensionFromVsix(vsixPath);
127 await config.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate); 129 await state.installedNightlyExtensionReleaseDate.set(releaseInfo.releaseDate);
128 await fs.unlink(vsixPath); 130 await fs.unlink(vsixPath);
129 131
130 await vscodeReloadWindow(); // never returns 132 await vscodeReloadWindow(); // never returns
diff --git a/editors/code/src/installation/server.ts b/editors/code/src/installation/server.ts
index 05730a778..05d326131 100644
--- a/editors/code/src/installation/server.ts
+++ b/editors/code/src/installation/server.ts
@@ -7,8 +7,9 @@ import { fetchArtifactReleaseInfo } from "./fetch_artifact_release_info";
7import { downloadArtifactWithProgressUi } from "./downloads"; 7import { downloadArtifactWithProgressUi } from "./downloads";
8import { log, assert, notReentrant } from "../util"; 8import { log, assert, notReentrant } from "../util";
9import { Config, NIGHTLY_TAG } from "../config"; 9import { Config, NIGHTLY_TAG } from "../config";
10import { PersistentState } from "../persistent_state";
10 11
11export async function ensureServerBinary(config: Config): Promise<null | string> { 12export async function ensureServerBinary(config: Config, state: PersistentState): Promise<null | string> {
12 const source = config.serverSource; 13 const source = config.serverSource;
13 14
14 if (!source) { 15 if (!source) {
@@ -37,7 +38,7 @@ export async function ensureServerBinary(config: Config): Promise<null | string>
37 return null; 38 return null;
38 } 39 }
39 case ArtifactSource.Type.GithubRelease: { 40 case ArtifactSource.Type.GithubRelease: {
40 if (!shouldDownloadServer(source, config)) { 41 if (!shouldDownloadServer(state, source)) {
41 return path.join(source.dir, source.file); 42 return path.join(source.dir, source.file);
42 } 43 }
43 44
@@ -50,24 +51,24 @@ export async function ensureServerBinary(config: Config): Promise<null | string>
50 if (userResponse !== "Download now") return null; 51 if (userResponse !== "Download now") return null;
51 } 52 }
52 53
53 return await downloadServer(source, config); 54 return await downloadServer(state, source);
54 } 55 }
55 } 56 }
56} 57}
57 58
58function shouldDownloadServer( 59function shouldDownloadServer(
60 state: PersistentState,
59 source: ArtifactSource.GithubRelease, 61 source: ArtifactSource.GithubRelease,
60 config: Config
61): boolean { 62): boolean {
62 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true; 63 if (!isBinaryAvailable(path.join(source.dir, source.file))) return true;
63 64
64 const installed = { 65 const installed = {
65 tag: config.serverReleaseTag.get(), 66 tag: state.serverReleaseTag.get(),
66 date: config.serverReleaseDate.get() 67 date: state.serverReleaseDate.get()
67 }; 68 };
68 const required = { 69 const required = {
69 tag: source.tag, 70 tag: source.tag,
70 date: config.installedNightlyExtensionReleaseDate.get() 71 date: state.installedNightlyExtensionReleaseDate.get()
71 }; 72 };
72 73
73 log.debug("Installed server:", installed, "required:", required); 74 log.debug("Installed server:", installed, "required:", required);
@@ -86,16 +87,16 @@ function shouldDownloadServer(
86 * Enforcing no reentrancy for this is best-effort. 87 * Enforcing no reentrancy for this is best-effort.
87 */ 88 */
88const downloadServer = notReentrant(async ( 89const downloadServer = notReentrant(async (
90 state: PersistentState,
89 source: ArtifactSource.GithubRelease, 91 source: ArtifactSource.GithubRelease,
90 config: Config,
91): Promise<null | string> => { 92): Promise<null | string> => {
92 try { 93 try {
93 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag); 94 const releaseInfo = await fetchArtifactReleaseInfo(source.repo, source.file, source.tag);
94 95
95 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server"); 96 await downloadArtifactWithProgressUi(releaseInfo, source.file, source.dir, "language server");
96 await Promise.all([ 97 await Promise.all([
97 config.serverReleaseTag.set(releaseInfo.releaseName), 98 state.serverReleaseTag.set(releaseInfo.releaseName),
98 config.serverReleaseDate.set(releaseInfo.releaseDate) 99 state.serverReleaseDate.set(releaseInfo.releaseDate)
99 ]); 100 ]);
100 } catch (err) { 101 } catch (err) {
101 log.downloadError(err, "language server", source.repo.name); 102 log.downloadError(err, "language server", source.repo.name);
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index bd4661a36..94ecd4dab 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -9,6 +9,7 @@ import { ensureServerBinary } from './installation/server';
9import { Config } from './config'; 9import { Config } from './config';
10import { log } from './util'; 10import { log } from './util';
11import { ensureProperExtensionVersion } from './installation/extension'; 11import { ensureProperExtensionVersion } from './installation/extension';
12import { PersistentState } from './persistent_state';
12 13
13let ctx: Ctx | undefined; 14let ctx: Ctx | undefined;
14 15
@@ -34,13 +35,14 @@ export async function activate(context: vscode.ExtensionContext) {
34 context.subscriptions.push(defaultOnEnter); 35 context.subscriptions.push(defaultOnEnter);
35 36
36 const config = new Config(context); 37 const config = new Config(context);
38 const state = new PersistentState(context);
37 39
38 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config).catch(log.error)); 40 vscode.workspace.onDidChangeConfiguration(() => ensureProperExtensionVersion(config, state).catch(log.error));
39 41
40 // Don't await the user response here, otherwise we will block the lsp server bootstrap 42 // Don't await the user response here, otherwise we will block the lsp server bootstrap
41 void ensureProperExtensionVersion(config).catch(log.error); 43 void ensureProperExtensionVersion(config, state).catch(log.error);
42 44
43 const serverPath = await ensureServerBinary(config); 45 const serverPath = await ensureServerBinary(config, state);
44 46
45 if (serverPath == null) { 47 if (serverPath == null) {
46 throw new Error( 48 throw new Error(
@@ -53,7 +55,7 @@ export async function activate(context: vscode.ExtensionContext) {
53 // registers its `onDidChangeDocument` handler before us. 55 // registers its `onDidChangeDocument` handler before us.
54 // 56 //
55 // This a horribly, horribly wrong way to deal with this problem. 57 // This a horribly, horribly wrong way to deal with this problem.
56 ctx = await Ctx.create(config, context, serverPath); 58 ctx = await Ctx.create(config, state, context, serverPath);
57 59
58 // Commands which invokes manually via command palette, shortcut, etc. 60 // Commands which invokes manually via command palette, shortcut, etc.
59 ctx.registerCommand('reload', (ctx) => { 61 ctx.registerCommand('reload', (ctx) => {
diff --git a/editors/code/src/persistent_state.ts b/editors/code/src/persistent_state.ts
new file mode 100644
index 000000000..13095b806
--- /dev/null
+++ b/editors/code/src/persistent_state.ts
@@ -0,0 +1,49 @@
1import * as vscode from 'vscode';
2import { log } from "./util";
3
4export class PersistentState {
5 constructor(private readonly ctx: vscode.ExtensionContext) {
6 }
7
8 readonly installedNightlyExtensionReleaseDate = new DateStorage(
9 "installed-nightly-extension-release-date",
10 this.ctx.globalState
11 );
12 readonly serverReleaseDate = new DateStorage("server-release-date", this.ctx.globalState);
13 readonly serverReleaseTag = new Storage<null | string>("server-release-tag", this.ctx.globalState, null);
14}
15
16
17export class Storage<T> {
18 constructor(
19 private readonly key: string,
20 private readonly storage: vscode.Memento,
21 private readonly defaultVal: T
22 ) { }
23
24 get(): T {
25 const val = this.storage.get(this.key, this.defaultVal);
26 log.debug(this.key, "==", val);
27 return val;
28 }
29 async set(val: T) {
30 log.debug(this.key, "=", val);
31 await this.storage.update(this.key, val);
32 }
33}
34export class DateStorage {
35 inner: Storage<null | string>;
36
37 constructor(key: string, storage: vscode.Memento) {
38 this.inner = new Storage(key, storage, null);
39 }
40
41 get(): null | Date {
42 const dateStr = this.inner.get();
43 return dateStr ? new Date(dateStr) : null;
44 }
45
46 async set(date: null | Date) {
47 await this.inner.set(date ? date.toString() : null);
48 }
49}
diff --git a/xtask/tests/tidy-tests/cli.rs b/xtask/tests/tidy-tests/cli.rs
index f9ca45292..f5b00a8b8 100644
--- a/xtask/tests/tidy-tests/cli.rs
+++ b/xtask/tests/tidy-tests/cli.rs
@@ -1,7 +1,6 @@
1use walkdir::WalkDir;
2use xtask::{ 1use xtask::{
3 codegen::{self, Mode}, 2 codegen::{self, Mode},
4 project_root, run_rustfmt, 3 run_rustfmt,
5}; 4};
6 5
7#[test] 6#[test]
@@ -31,25 +30,3 @@ fn check_code_formatting() {
31 panic!("{}. Please format the code by running `cargo format`", error); 30 panic!("{}. Please format the code by running `cargo format`", error);
32 } 31 }
33} 32}
34
35#[test]
36fn no_todo() {
37 WalkDir::new(project_root().join("crates")).into_iter().for_each(|e| {
38 let e = e.unwrap();
39 if e.path().extension().map(|it| it != "rs").unwrap_or(true) {
40 return;
41 }
42 if e.path().ends_with("tests/cli.rs") {
43 return;
44 }
45 let text = std::fs::read_to_string(e.path()).unwrap();
46 if text.contains("TODO") || text.contains("TOOD") || text.contains("todo!") {
47 panic!(
48 "\nTODO markers should not be committed to the master branch,\n\
49 use FIXME instead\n\
50 {}\n",
51 e.path().display(),
52 )
53 }
54 })
55}
diff --git a/xtask/tests/tidy-tests/docs.rs b/xtask/tests/tidy-tests/docs.rs
deleted file mode 100644
index 62c4f8441..000000000
--- a/xtask/tests/tidy-tests/docs.rs
+++ /dev/null
@@ -1,106 +0,0 @@
1use std::{collections::HashMap, fs, io::prelude::*, io::BufReader, path::Path};
2
3use anyhow::Context;
4use walkdir::{DirEntry, WalkDir};
5use xtask::project_root;
6
7fn is_exclude_dir(p: &Path) -> bool {
8 // Test hopefully don't really need comments, and for assists we already
9 // have special comments which are source of doc tests and user docs.
10 let exclude_dirs = ["tests", "test_data", "handlers"];
11 let mut cur_path = p;
12 while let Some(path) = cur_path.parent() {
13 if exclude_dirs.iter().any(|dir| path.ends_with(dir)) {
14 return true;
15 }
16 cur_path = path;
17 }
18
19 false
20}
21
22fn is_exclude_file(d: &DirEntry) -> bool {
23 let file_names = ["tests.rs"];
24
25 d.file_name().to_str().map(|f_n| file_names.iter().any(|name| *name == f_n)).unwrap_or(false)
26}
27
28fn is_hidden(entry: &DirEntry) -> bool {
29 entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
30}
31
32#[test]
33fn no_docs_comments() {
34 let crates = project_root().join("crates");
35 let iter = WalkDir::new(crates);
36 let mut missing_docs = Vec::new();
37 let mut contains_fixme = Vec::new();
38 for f in iter.into_iter().filter_entry(|e| !is_hidden(e)) {
39 let f = f.unwrap();
40 if f.file_type().is_dir() {
41 continue;
42 }
43 if f.path().extension().map(|it| it != "rs").unwrap_or(false) {
44 continue;
45 }
46 if is_exclude_dir(f.path()) {
47 continue;
48 }
49 if is_exclude_file(&f) {
50 continue;
51 }
52 let mut reader = BufReader::new(fs::File::open(f.path()).unwrap());
53 let mut line = String::new();
54 reader
55 .read_line(&mut line)
56 .with_context(|| format!("Failed to read {}", f.path().display()))
57 .unwrap();
58
59 if line.starts_with("//!") {
60 if line.contains("FIXME") {
61 contains_fixme.push(f.path().to_path_buf())
62 }
63 } else {
64 missing_docs.push(f.path().display().to_string());
65 }
66 }
67 if !missing_docs.is_empty() {
68 panic!(
69 "\nMissing docs strings\n\n\
70 modules:\n{}\n\n",
71 missing_docs.join("\n")
72 )
73 }
74
75 let whitelist = [
76 "ra_db",
77 "ra_hir",
78 "ra_hir_expand",
79 "ra_ide",
80 "ra_mbe",
81 "ra_parser",
82 "ra_prof",
83 "ra_project_model",
84 "ra_syntax",
85 "ra_text_edit",
86 "ra_tt",
87 "ra_hir_ty",
88 ];
89
90 let mut has_fixmes = whitelist.iter().map(|it| (*it, false)).collect::<HashMap<&str, bool>>();
91 'outer: for path in contains_fixme {
92 for krate in whitelist.iter() {
93 if path.components().any(|it| it.as_os_str() == *krate) {
94 has_fixmes.insert(krate, true);
95 continue 'outer;
96 }
97 }
98 panic!("FIXME doc in a fully-documented crate: {}", path.display())
99 }
100
101 for (krate, has_fixme) in has_fixmes.iter() {
102 if !has_fixme {
103 panic!("crate {} is fully documented, remove it from the white list", krate)
104 }
105 }
106}
diff --git a/xtask/tests/tidy-tests/main.rs b/xtask/tests/tidy-tests/main.rs
index 56d1318d6..2d2d88bec 100644
--- a/xtask/tests/tidy-tests/main.rs
+++ b/xtask/tests/tidy-tests/main.rs
@@ -1,2 +1,145 @@
1mod cli; 1mod cli;
2mod docs; 2
3use std::{
4 collections::HashMap,
5 path::{Path, PathBuf},
6};
7
8use walkdir::{DirEntry, WalkDir};
9use xtask::{not_bash::fs2, project_root};
10
11#[test]
12fn rust_files_are_tidy() {
13 let mut tidy_docs = TidyDocs::default();
14 for path in rust_files() {
15 let text = fs2::read_to_string(&path).unwrap();
16 check_todo(&path, &text);
17 tidy_docs.visit(&path, &text);
18 }
19 tidy_docs.finish();
20}
21
22fn check_todo(path: &Path, text: &str) {
23 if path.ends_with("tests/cli.rs") {
24 return;
25 }
26 if text.contains("TODO") || text.contains("TOOD") || text.contains("todo!") {
27 panic!(
28 "\nTODO markers should not be committed to the master branch,\n\
29 use FIXME instead\n\
30 {}\n",
31 path.display(),
32 )
33 }
34}
35
36#[derive(Default)]
37struct TidyDocs {
38 missing_docs: Vec<String>,
39 contains_fixme: Vec<PathBuf>,
40}
41
42impl TidyDocs {
43 fn visit(&mut self, path: &Path, text: &str) {
44 if is_exclude_dir(path) || is_exclude_file(path) {
45 return;
46 }
47
48 let first_line = match text.lines().next() {
49 Some(it) => it,
50 None => return,
51 };
52
53 if first_line.starts_with("//!") {
54 if first_line.contains("FIXME") {
55 self.contains_fixme.push(path.to_path_buf())
56 }
57 } else {
58 self.missing_docs.push(path.display().to_string());
59 }
60
61 fn is_exclude_dir(p: &Path) -> bool {
62 // Test hopefully don't really need comments, and for assists we already
63 // have special comments which are source of doc tests and user docs.
64 let exclude_dirs = ["tests", "test_data", "handlers"];
65 let mut cur_path = p;
66 while let Some(path) = cur_path.parent() {
67 if exclude_dirs.iter().any(|dir| path.ends_with(dir)) {
68 return true;
69 }
70 cur_path = path;
71 }
72
73 false
74 }
75
76 fn is_exclude_file(d: &Path) -> bool {
77 let file_names = ["tests.rs"];
78
79 d.file_name()
80 .unwrap_or_default()
81 .to_str()
82 .map(|f_n| file_names.iter().any(|name| *name == f_n))
83 .unwrap_or(false)
84 }
85 }
86
87 fn finish(self) {
88 if !self.missing_docs.is_empty() {
89 panic!(
90 "\nMissing docs strings\n\n\
91 modules:\n{}\n\n",
92 self.missing_docs.join("\n")
93 )
94 }
95
96 let whitelist = [
97 "ra_db",
98 "ra_hir",
99 "ra_hir_expand",
100 "ra_ide",
101 "ra_mbe",
102 "ra_parser",
103 "ra_prof",
104 "ra_project_model",
105 "ra_syntax",
106 "ra_text_edit",
107 "ra_tt",
108 "ra_hir_ty",
109 ];
110
111 let mut has_fixmes =
112 whitelist.iter().map(|it| (*it, false)).collect::<HashMap<&str, bool>>();
113 'outer: for path in self.contains_fixme {
114 for krate in whitelist.iter() {
115 if path.components().any(|it| it.as_os_str() == *krate) {
116 has_fixmes.insert(krate, true);
117 continue 'outer;
118 }
119 }
120 panic!("FIXME doc in a fully-documented crate: {}", path.display())
121 }
122
123 for (krate, has_fixme) in has_fixmes.iter() {
124 if !has_fixme {
125 panic!("crate {} is fully documented, remove it from the white list", krate)
126 }
127 }
128 }
129}
130
131fn rust_files() -> impl Iterator<Item = PathBuf> {
132 let crates = project_root().join("crates");
133 let iter = WalkDir::new(crates);
134 return iter
135 .into_iter()
136 .filter_entry(|e| !is_hidden(e))
137 .map(|e| e.unwrap())
138 .filter(|e| !e.file_type().is_dir())
139 .map(|e| e.into_path())
140 .filter(|path| path.extension().map(|it| it == "rs").unwrap_or(false));
141
142 fn is_hidden(entry: &DirEntry) -> bool {
143 entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
144 }
145}