Skip to main content

x_lint/
lib.rs

1// Copyright (c) The Diem Core Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Lint engine.
5//!
6//! The overall design is generally inspired by
7//! [Arcanist](https://secure.phabricator.com/book/phabricator/article/arcanist_lint)'s lint engine.
8
9pub mod content;
10pub mod file_path;
11pub mod package;
12pub mod project;
13pub mod runner;
14
15use camino::Utf8Path;
16use guppy::PackageId;
17use std::{borrow::Cow, fmt};
18
19/// Represents a linter.
20pub trait Linter: Send + Sync + fmt::Debug {
21    /// Returns the name of the linter.
22    fn name(&self) -> &'static str;
23}
24
25/// Represents common functionality among various `Context` instances.
26trait LintContext<'l> {
27    /// Returns the kind of this lint context.
28    fn kind(&self) -> LintKind<'l>;
29
30    /// Returns a `LintSource` for this lint context.
31    fn source(&self, name: &'static str) -> LintSource<'l> {
32        LintSource::new(name, self.kind())
33    }
34}
35
36/// A lint formatter.
37///
38/// Lints write `LintMessage` instances to this.
39pub struct LintFormatter<'l, 'a> {
40    source: LintSource<'l>,
41    messages: &'a mut Vec<(LintSource<'l>, LintMessage)>,
42}
43
44impl<'l, 'a> LintFormatter<'l, 'a> {
45    pub fn new(
46        source: LintSource<'l>,
47        messages: &'a mut Vec<(LintSource<'l>, LintMessage)>,
48    ) -> Self {
49        Self { source, messages }
50    }
51
52    /// Writes a new lint message to this formatter.
53    pub fn write(&mut self, level: LintLevel, message: impl Into<Cow<'static, str>>) {
54        self.messages
55            .push((self.source, LintMessage::new(level, message)));
56    }
57
58    /// Writes a new lint message to this formatter with a custom kind.
59    pub fn write_kind(
60        &mut self,
61        kind: LintKind<'l>,
62        level: LintLevel,
63        message: impl Into<Cow<'static, str>>,
64    ) {
65        self.messages.push((
66            LintSource::new(self.source.name(), kind),
67            LintMessage::new(level, message),
68        ));
69    }
70}
71
72/// The run status of a lint.
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub enum RunStatus<'l> {
75    /// This lint run was successful, with messages possibly written into the `LintFormatter`.
76    Executed,
77    /// This lint was skipped.
78    Skipped(SkipReason<'l>),
79}
80
81/// The reason for why this lint was skipped.
82#[derive(Clone, Debug, Eq, PartialEq)]
83#[non_exhaustive]
84pub enum SkipReason<'l> {
85    /// This file's content was not valid UTF-8.
86    NonUtf8Content,
87    /// This extension was unsupported.
88    UnsupportedExtension(Option<&'l str>),
89    /// The given file was unsupported by this linter.
90    UnsupportedFile(&'l Utf8Path),
91    /// The given package was unsupported by this linter.
92    UnsupportedPackage(&'l PackageId),
93    /// The given file was excepted by a glob rule
94    GlobExemption(&'l str),
95    // TODO: Add more reasons.
96}
97
98/// A message raised by a lint.
99#[derive(Debug)]
100pub struct LintMessage {
101    level: LintLevel,
102    message: Cow<'static, str>,
103}
104
105impl LintMessage {
106    pub fn new(level: LintLevel, message: impl Into<Cow<'static, str>>) -> Self {
107        Self {
108            level,
109            message: message.into(),
110        }
111    }
112
113    pub fn level(&self) -> LintLevel {
114        self.level
115    }
116
117    pub fn message(&self) -> &str {
118        &self.message
119    }
120}
121
122#[derive(Copy, Clone, Debug, Eq, PartialEq)]
123#[allow(dead_code)]
124#[non_exhaustive]
125pub enum LintLevel {
126    Error,
127    Warning,
128    // TODO: add more levels?
129}
130
131impl fmt::Display for LintLevel {
132    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
133        match self {
134            LintLevel::Error => write!(f, "ERROR"),
135            LintLevel::Warning => write!(f, "WARNING"),
136        }
137    }
138}
139
140/// Message source for lints.
141#[derive(Copy, Clone, Debug)]
142pub struct LintSource<'l> {
143    name: &'static str,
144    kind: LintKind<'l>,
145}
146
147impl<'l> LintSource<'l> {
148    fn new(name: &'static str, kind: LintKind<'l>) -> Self {
149        Self { name, kind }
150    }
151
152    pub fn name(&self) -> &'static str {
153        self.name
154    }
155
156    pub fn kind(&self) -> LintKind<'l> {
157        self.kind
158    }
159}
160
161#[derive(Copy, Clone, Debug, Eq, PartialEq)]
162pub enum LintKind<'l> {
163    Project,
164    Package {
165        name: &'l str,
166        workspace_path: &'l Utf8Path,
167    },
168    FilePath(&'l Utf8Path),
169    Content(&'l Utf8Path),
170}
171
172impl<'l> fmt::Display for LintKind<'l> {
173    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
174        match self {
175            LintKind::Project => write!(f, "project"),
176            LintKind::Package {
177                name,
178                workspace_path,
179            } => write!(f, "package '{}' (at {})", name, workspace_path),
180            LintKind::FilePath(path) => write!(f, "file path {}", path),
181            LintKind::Content(path) => write!(f, "content {}", path),
182        }
183    }
184}
185
186pub mod prelude {
187    pub use super::{
188        content::{ContentContext, ContentLinter},
189        file_path::{FilePathContext, FilePathLinter},
190        package::{PackageContext, PackageLinter},
191        project::{ProjectContext, ProjectLinter},
192        runner::{LintEngine, LintEngineConfig, LintResults},
193        LintFormatter, LintKind, LintLevel, LintMessage, LintSource, Linter, RunStatus, SkipReason,
194    };
195    pub use x_core::{Result, SystemError};
196}