sonicop 26.8.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CONFORMANCE.md +109 -0
- data/Cargo.lock +975 -0
- data/Cargo.toml +40 -0
- data/LICENSE +21 -0
- data/NOTICE +7 -0
- data/README.ja.md +161 -0
- data/README.md +173 -0
- data/config/default.yml +6305 -0
- data/exe/sonicop +7 -0
- data/ext/sonicop/extconf.rb +38 -0
- data/lib/sonicop/runner.rb +67 -0
- data/lib/sonicop/version.rb +6 -0
- data/lib/sonicop.rb +8 -0
- data/licenses/RUBOCOP.txt +20 -0
- data/licenses/TREE_SITTER_RUBY.txt +21 -0
- data/src/cli.rs +940 -0
- data/src/config/inheritance.rs +332 -0
- data/src/config/loader.rs +117 -0
- data/src/config/mod.rs +461 -0
- data/src/config/paths.rs +436 -0
- data/src/config/plugin.rs +247 -0
- data/src/config/store.rs +141 -0
- data/src/cop_name.rs +71 -0
- data/src/diagnostic.rs +205 -0
- data/src/directives.rs +305 -0
- data/src/engine.rs +734 -0
- data/src/formatter.rs +684 -0
- data/src/lib.rs +42 -0
- data/src/main.rs +3 -0
- data/src/ruby_version.rs +393 -0
- data/src/rules/layout/empty_line_after_magic_comment.rs +49 -0
- data/src/rules/layout/end_of_line.rs +35 -0
- data/src/rules/layout/line_length.rs +167 -0
- data/src/rules/layout/mod.rs +13 -0
- data/src/rules/layout/space_after_comma.rs +32 -0
- data/src/rules/layout/space_around_operators.rs +104 -0
- data/src/rules/layout/space_inside_parens.rs +100 -0
- data/src/rules/layout/support.rs +23 -0
- data/src/rules/layout/trailing_empty_lines.rs +38 -0
- data/src/rules/layout/trailing_whitespace.rs +26 -0
- data/src/rules/lint/duplicate_methods.rs +72 -0
- data/src/rules/lint/mod.rs +7 -0
- data/src/rules/lint/syntax.rs +200 -0
- data/src/rules/lint/unused_block_argument.rs +74 -0
- data/src/rules/lint/useless_assignment.rs +104 -0
- data/src/rules/metrics/block_length.rs +39 -0
- data/src/rules/metrics/class_length.rs +34 -0
- data/src/rules/metrics/method_length.rs +10 -0
- data/src/rules/metrics/mod.rs +10 -0
- data/src/rules/metrics/module_length.rs +17 -0
- data/src/rules/metrics/parameter_lists.rs +21 -0
- data/src/rules/metrics/support.rs +105 -0
- data/src/rules/mod.rs +380 -0
- data/src/rules/naming/ascii_identifiers.rs +17 -0
- data/src/rules/naming/constant_name.rs +49 -0
- data/src/rules/naming/method_name.rs +51 -0
- data/src/rules/naming/mod.rs +9 -0
- data/src/rules/naming/support.rs +22 -0
- data/src/rules/naming/variable_name.rs +50 -0
- data/src/rules/security/eval.rs +170 -0
- data/src/rules/security/mod.rs +6 -0
- data/src/rules/style/frozen_string_literal_comment.rs +56 -0
- data/src/rules/style/hash_syntax.rs +63 -0
- data/src/rules/style/mod.rs +9 -0
- data/src/rules/style/numeric_literals.rs +59 -0
- data/src/rules/style/redundant_return.rs +143 -0
- data/src/rules/style/semicolon.rs +48 -0
- data/src/rules/style/string_literals.rs +74 -0
- data/src/rules/support.rs +31 -0
- data/src/source.rs +118 -0
- metadata +117 -0
data/src/config/store.rs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::{Path, PathBuf};
|
|
4
|
+
use std::sync::{Arc, Mutex};
|
|
5
|
+
|
|
6
|
+
use anyhow::{Result, bail};
|
|
7
|
+
|
|
8
|
+
use super::Config;
|
|
9
|
+
use super::loader::find_config;
|
|
10
|
+
|
|
11
|
+
type ConfigCache = Mutex<HashMap<PathBuf, Arc<Config>>>;
|
|
12
|
+
|
|
13
|
+
#[derive(Debug)]
|
|
14
|
+
pub struct ConfigStore {
|
|
15
|
+
root: Arc<Config>,
|
|
16
|
+
discover_per_path: bool,
|
|
17
|
+
ignore_unrecognized: bool,
|
|
18
|
+
directories: ConfigCache,
|
|
19
|
+
cache: ConfigCache,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
impl ConfigStore {
|
|
23
|
+
pub fn new(config: Config, discover_per_path: bool, ignore_unrecognized: bool) -> Self {
|
|
24
|
+
Self {
|
|
25
|
+
root: Arc::new(config),
|
|
26
|
+
discover_per_path,
|
|
27
|
+
ignore_unrecognized,
|
|
28
|
+
directories: Mutex::new(HashMap::new()),
|
|
29
|
+
cache: Mutex::new(HashMap::new()),
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn root(&self) -> &Config {
|
|
34
|
+
&self.root
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn for_path(&self, path: &Path) -> Result<Arc<Config>> {
|
|
38
|
+
if !self.discover_per_path {
|
|
39
|
+
return Ok(Arc::clone(&self.root));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let start = if path.is_dir() {
|
|
43
|
+
path
|
|
44
|
+
} else {
|
|
45
|
+
path.parent()
|
|
46
|
+
.filter(|parent| !parent.as_os_str().is_empty())
|
|
47
|
+
.unwrap_or(Path::new("."))
|
|
48
|
+
};
|
|
49
|
+
// `find_config` canonicalizes and stats every ancestor directory, so keying only
|
|
50
|
+
// the resolved configuration path made every file pay that cost again. Directories
|
|
51
|
+
// are far fewer than the files below them, so memoize on the directory itself.
|
|
52
|
+
if let Some(config) = cached(&self.directories, start)? {
|
|
53
|
+
return Ok(config);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let discovered = find_config(start);
|
|
57
|
+
let config = if discovered.as_deref() == self.root.config_path() {
|
|
58
|
+
Arc::clone(&self.root)
|
|
59
|
+
} else {
|
|
60
|
+
let key = discovered
|
|
61
|
+
.clone()
|
|
62
|
+
.unwrap_or_else(|| fs::canonicalize(start).unwrap_or_else(|_| start.to_path_buf()));
|
|
63
|
+
match cached(&self.cache, &key)? {
|
|
64
|
+
Some(config) => config,
|
|
65
|
+
None => {
|
|
66
|
+
let config = Arc::new(Config::load(discovered.as_deref(), start)?);
|
|
67
|
+
if !self.ignore_unrecognized && !config.unrecognized_cop_names().is_empty() {
|
|
68
|
+
bail!(
|
|
69
|
+
"unrecognized cop(s): {}",
|
|
70
|
+
config.unrecognized_cop_names().join(", ")
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
store(&self.cache, key, &config)?;
|
|
74
|
+
config
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
store(&self.directories, start.to_path_buf(), &config)?;
|
|
79
|
+
Ok(config)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fn cached(cache: &ConfigCache, key: &Path) -> Result<Option<Arc<Config>>> {
|
|
84
|
+
Ok(lock(cache)?.get(key).cloned())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
fn store(cache: &ConfigCache, key: PathBuf, config: &Arc<Config>) -> Result<()> {
|
|
88
|
+
lock(cache)?.insert(key, Arc::clone(config));
|
|
89
|
+
Ok(())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn lock(cache: &ConfigCache) -> Result<std::sync::MutexGuard<'_, HashMap<PathBuf, Arc<Config>>>> {
|
|
93
|
+
cache
|
|
94
|
+
.lock()
|
|
95
|
+
.map_err(|_| anyhow::anyhow!("configuration cache lock is poisoned"))
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[cfg(test)]
|
|
99
|
+
mod tests {
|
|
100
|
+
use std::fs;
|
|
101
|
+
|
|
102
|
+
use tempfile::tempdir;
|
|
103
|
+
|
|
104
|
+
use super::{Config, ConfigStore};
|
|
105
|
+
|
|
106
|
+
#[test]
|
|
107
|
+
fn store_resolves_nested_configuration_from_target_path() {
|
|
108
|
+
let directory = tempdir().unwrap();
|
|
109
|
+
let nested = directory.path().join("nested");
|
|
110
|
+
fs::create_dir(&nested).unwrap();
|
|
111
|
+
fs::write(directory.path().join("Gemfile"), "").unwrap();
|
|
112
|
+
fs::write(
|
|
113
|
+
directory.path().join(".rubocop.yml"),
|
|
114
|
+
"AllCops:\n DisabledByDefault: true\nLayout/TrailingWhitespace:\n Enabled: true\n",
|
|
115
|
+
)
|
|
116
|
+
.unwrap();
|
|
117
|
+
fs::write(
|
|
118
|
+
nested.join(".rubocop.yml"),
|
|
119
|
+
"inherit_from: ../.rubocop.yml\nLayout/TrailingWhitespace:\n Enabled: false\n",
|
|
120
|
+
)
|
|
121
|
+
.unwrap();
|
|
122
|
+
|
|
123
|
+
let root = Config::load(None, directory.path()).unwrap();
|
|
124
|
+
let store = ConfigStore::new(root, true, false);
|
|
125
|
+
let root_config = store.for_path(&directory.path().join("root.rb")).unwrap();
|
|
126
|
+
let nested_config = store.for_path(&nested.join("nested.rb")).unwrap();
|
|
127
|
+
|
|
128
|
+
assert!(root_config.rule_enabled("Layout/TrailingWhitespace"));
|
|
129
|
+
assert!(!nested_config.rule_enabled("Layout/TrailingWhitespace"));
|
|
130
|
+
assert_eq!(
|
|
131
|
+
nested_config.config_path(),
|
|
132
|
+
Some(
|
|
133
|
+
nested
|
|
134
|
+
.join(".rubocop.yml")
|
|
135
|
+
.canonicalize()
|
|
136
|
+
.unwrap()
|
|
137
|
+
.as_path()
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
data/src/cop_name.rs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//! How a cop's qualified name decomposes, and when a user-supplied selector names it.
|
|
2
|
+
//!
|
|
3
|
+
//! RuboCop settles both questions in `Cop::Badge` and `Cop::Registry`. Keeping them in one place
|
|
4
|
+
//! here is what stops `--only`, `# rubocop:disable`, configuration inheritance and `--show-cops`
|
|
5
|
+
//! from disagreeing about a nested cop such as `Chef/Correctness/ServiceResource`.
|
|
6
|
+
|
|
7
|
+
/// Everything before the last `/`, or the whole name when it has none.
|
|
8
|
+
///
|
|
9
|
+
/// `Badge#initialize` joins every segment but the last, so the department of
|
|
10
|
+
/// `Chef/Correctness/ServiceResource` is `Chef/Correctness` rather than `Chef`.
|
|
11
|
+
pub fn department(cop_name: &str) -> &str {
|
|
12
|
+
cop_name
|
|
13
|
+
.rsplit_once('/')
|
|
14
|
+
.map_or(cop_name, |(department, _)| department)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// The cop's department and every namespace enclosing it, the department itself first.
|
|
18
|
+
///
|
|
19
|
+
/// Only plugin ownership widens like this: a gem that declares `I18n` also ships the nested
|
|
20
|
+
/// `I18n/GetText` department. Selectors and configuration lookups must keep using [`department`],
|
|
21
|
+
/// which stops at the one department the cop actually belongs to.
|
|
22
|
+
pub fn department_ancestors(cop_name: &str) -> impl Iterator<Item = &str> {
|
|
23
|
+
let department = department(cop_name);
|
|
24
|
+
std::iter::once(department).chain(
|
|
25
|
+
department
|
|
26
|
+
.match_indices('/')
|
|
27
|
+
.map(move |(offset, _)| &department[..offset]),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Whether a selector the user wrote -- in `--only`, `--except`, or a `rubocop:disable` comment --
|
|
32
|
+
/// names this cop.
|
|
33
|
+
///
|
|
34
|
+
/// `Badge#match_name?` compares the qualified name and the department, both in full, so an outer
|
|
35
|
+
/// namespace does not reach a nested cop: `Chef` leaves `Chef/Correctness/ServiceResource` alone
|
|
36
|
+
/// and only `Chef/Correctness` selects it.
|
|
37
|
+
pub fn selector_matches(selector: &str, cop_name: &str) -> bool {
|
|
38
|
+
selector == cop_name || selector == department(cop_name)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
mod tests {
|
|
43
|
+
use super::{department, selector_matches};
|
|
44
|
+
|
|
45
|
+
#[test]
|
|
46
|
+
fn department_is_every_segment_but_the_last() {
|
|
47
|
+
assert_eq!(department("Layout/LineLength"), "Layout");
|
|
48
|
+
assert_eq!(
|
|
49
|
+
department("Chef/Correctness/ServiceResource"),
|
|
50
|
+
"Chef/Correctness"
|
|
51
|
+
);
|
|
52
|
+
assert_eq!(department("Syntax"), "Syntax");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[test]
|
|
56
|
+
fn a_selector_names_a_cop_by_full_name_or_whole_department() {
|
|
57
|
+
assert!(selector_matches("Layout/LineLength", "Layout/LineLength"));
|
|
58
|
+
assert!(selector_matches("Layout", "Layout/LineLength"));
|
|
59
|
+
assert!(!selector_matches("Lay", "Layout/LineLength"));
|
|
60
|
+
assert!(!selector_matches("Layout/Line", "Layout/LineLength"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// An outer namespace is not a department of its own, so it selects nothing below it.
|
|
64
|
+
#[test]
|
|
65
|
+
fn an_outer_namespace_does_not_select_a_nested_cop() {
|
|
66
|
+
let cop = "Chef/Correctness/ServiceResource";
|
|
67
|
+
assert!(selector_matches("Chef/Correctness", cop));
|
|
68
|
+
assert!(!selector_matches("Chef", cop));
|
|
69
|
+
assert!(selector_matches(cop, cop));
|
|
70
|
+
}
|
|
71
|
+
}
|
data/src/diagnostic.rs
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
use std::fmt;
|
|
2
|
+
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
|
|
5
|
+
use crate::source::SourceFile;
|
|
6
|
+
|
|
7
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
|
|
8
|
+
#[serde(rename_all = "lowercase")]
|
|
9
|
+
pub enum Severity {
|
|
10
|
+
Info,
|
|
11
|
+
Refactor,
|
|
12
|
+
Convention,
|
|
13
|
+
Warning,
|
|
14
|
+
Error,
|
|
15
|
+
Fatal,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl Severity {
|
|
19
|
+
/// The canonical RuboCop name (`Severity#to_s`). Every textual rendering derives from this so
|
|
20
|
+
/// that renaming a variant cannot silently change user-visible output.
|
|
21
|
+
pub fn as_str(self) -> &'static str {
|
|
22
|
+
match self {
|
|
23
|
+
Self::Info => "info",
|
|
24
|
+
Self::Refactor => "refactor",
|
|
25
|
+
Self::Convention => "convention",
|
|
26
|
+
Self::Warning => "warning",
|
|
27
|
+
Self::Error => "error",
|
|
28
|
+
Self::Fatal => "fatal",
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub fn code(self) -> char {
|
|
33
|
+
match self {
|
|
34
|
+
Self::Info => 'I',
|
|
35
|
+
Self::Refactor => 'R',
|
|
36
|
+
Self::Convention => 'C',
|
|
37
|
+
Self::Warning => 'W',
|
|
38
|
+
Self::Error => 'E',
|
|
39
|
+
Self::Fatal => 'F',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn parse(value: &str) -> Option<Self> {
|
|
44
|
+
match value.to_ascii_lowercase().as_str() {
|
|
45
|
+
"info" | "i" => Some(Self::Info),
|
|
46
|
+
"refactor" | "r" => Some(Self::Refactor),
|
|
47
|
+
"convention" | "c" => Some(Self::Convention),
|
|
48
|
+
"warning" | "w" => Some(Self::Warning),
|
|
49
|
+
"error" | "e" => Some(Self::Error),
|
|
50
|
+
"fatal" | "f" => Some(Self::Fatal),
|
|
51
|
+
_ => None,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl fmt::Display for Severity {
|
|
57
|
+
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
58
|
+
formatter.write_str(self.as_str())
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
#[derive(Clone, Debug)]
|
|
63
|
+
pub struct Edit {
|
|
64
|
+
pub start: usize,
|
|
65
|
+
pub end: usize,
|
|
66
|
+
pub replacement: String,
|
|
67
|
+
pub safe: bool,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Position and source line captured from the text an offense was found in. An autocorrect pass
|
|
71
|
+
/// rewrites that text, so a corrected offense carried into the final report can no longer resolve
|
|
72
|
+
/// its byte offsets against the report it travels with.
|
|
73
|
+
#[derive(Clone, Debug)]
|
|
74
|
+
pub struct OffenseSnapshot {
|
|
75
|
+
pub location: Location,
|
|
76
|
+
pub source_line: String,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#[derive(Clone, Debug)]
|
|
80
|
+
pub struct Offense {
|
|
81
|
+
pub cop_name: &'static str,
|
|
82
|
+
pub severity: Severity,
|
|
83
|
+
pub message: String,
|
|
84
|
+
pub start: usize,
|
|
85
|
+
pub end: usize,
|
|
86
|
+
pub corrected: bool,
|
|
87
|
+
pub correctable: bool,
|
|
88
|
+
pub suppressed: bool,
|
|
89
|
+
pub justification: Option<String>,
|
|
90
|
+
pub correction: Option<Edit>,
|
|
91
|
+
pub snapshot: Option<OffenseSnapshot>,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl Offense {
|
|
95
|
+
/// Cops do not call this: `RuleContext::offense` supplies the name and severity from the
|
|
96
|
+
/// registry so that a cop never names itself. It stays reachable inside the crate for the
|
|
97
|
+
/// engine's own offenses, which belong to no cop -- a file that is not valid UTF-8 never
|
|
98
|
+
/// reaches one.
|
|
99
|
+
pub(crate) fn new(
|
|
100
|
+
cop_name: &'static str,
|
|
101
|
+
severity: Severity,
|
|
102
|
+
message: impl Into<String>,
|
|
103
|
+
start: usize,
|
|
104
|
+
end: usize,
|
|
105
|
+
) -> Self {
|
|
106
|
+
Self {
|
|
107
|
+
cop_name,
|
|
108
|
+
severity,
|
|
109
|
+
message: message.into(),
|
|
110
|
+
start,
|
|
111
|
+
end: end.max(start),
|
|
112
|
+
corrected: false,
|
|
113
|
+
correctable: false,
|
|
114
|
+
suppressed: false,
|
|
115
|
+
justification: None,
|
|
116
|
+
correction: None,
|
|
117
|
+
snapshot: None,
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn corrected_by(mut self, edit: Edit) -> Self {
|
|
122
|
+
self.correctable = true;
|
|
123
|
+
self.correction = Some(edit);
|
|
124
|
+
self
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// RuboCop derives `correctable?` from the offense status, so an offense a directive comment
|
|
128
|
+
/// suppressed is never correctable however the cop flagged it. Every count, filter and exit
|
|
129
|
+
/// code decision goes through here rather than reading the raw flag.
|
|
130
|
+
pub fn is_correctable(&self) -> bool {
|
|
131
|
+
self.correctable && !self.suppressed
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// Freeze the position and source line so the offense keeps reporting against the text it was
|
|
135
|
+
/// found in once autocorrect replaces that text.
|
|
136
|
+
pub fn freeze_location(&mut self, source: &SourceFile) {
|
|
137
|
+
if self.snapshot.is_some() {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let location = self.location(source);
|
|
141
|
+
self.snapshot = Some(OffenseSnapshot {
|
|
142
|
+
location,
|
|
143
|
+
source_line: source.line(location.line).to_owned(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Where the offense starts. Ordering and identity only ever need this, and resolving it alone
|
|
148
|
+
/// avoids touching the end of the range, which callers may not have placed on a char boundary.
|
|
149
|
+
pub fn start_position(&self, source: &SourceFile) -> (usize, usize) {
|
|
150
|
+
match &self.snapshot {
|
|
151
|
+
Some(snapshot) => (snapshot.location.line, snapshot.location.column),
|
|
152
|
+
None => source.line_column(self.start),
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// The source line the offense points at, taken from the frozen snapshot when the report's own
|
|
157
|
+
/// text has since been rewritten.
|
|
158
|
+
pub fn source_line<'a>(&'a self, source: &'a SourceFile) -> &'a str {
|
|
159
|
+
match &self.snapshot {
|
|
160
|
+
Some(snapshot) => &snapshot.source_line,
|
|
161
|
+
None => source.line(self.location(source).line),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
pub fn location(&self, source: &SourceFile) -> Location {
|
|
166
|
+
if let Some(snapshot) = &self.snapshot {
|
|
167
|
+
return snapshot.location;
|
|
168
|
+
}
|
|
169
|
+
let (start_line, start_column) = source.line_column(self.start);
|
|
170
|
+
// Stepping back one byte from the exclusive end can land inside a multibyte character,
|
|
171
|
+
// which `line_column` cannot slice at; walk back to the character that byte belongs to.
|
|
172
|
+
let mut inclusive_end = self.end.saturating_sub(1).max(self.start);
|
|
173
|
+
while inclusive_end > self.start && !source.text().is_char_boundary(inclusive_end) {
|
|
174
|
+
inclusive_end -= 1;
|
|
175
|
+
}
|
|
176
|
+
let (last_line, last_column) = source.line_column(inclusive_end);
|
|
177
|
+
Location {
|
|
178
|
+
start_line,
|
|
179
|
+
start_column,
|
|
180
|
+
last_line,
|
|
181
|
+
last_column,
|
|
182
|
+
length: self.end.saturating_sub(self.start),
|
|
183
|
+
line: start_line,
|
|
184
|
+
column: start_column,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[derive(Clone, Copy, Debug, Serialize)]
|
|
190
|
+
pub struct Location {
|
|
191
|
+
pub start_line: usize,
|
|
192
|
+
pub start_column: usize,
|
|
193
|
+
pub last_line: usize,
|
|
194
|
+
pub last_column: usize,
|
|
195
|
+
pub length: usize,
|
|
196
|
+
pub line: usize,
|
|
197
|
+
pub column: usize,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
#[derive(Clone, Debug)]
|
|
201
|
+
pub struct FileReport {
|
|
202
|
+
pub path: std::path::PathBuf,
|
|
203
|
+
pub source: SourceFile,
|
|
204
|
+
pub offenses: Vec<Offense>,
|
|
205
|
+
}
|