Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions harper-cli/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub struct LintOptions {
pub weirpack_inputs: Vec<SingleInput>,
pub color: bool,
pub format: OutputFormat,
pub quiet: bool,
}

enum ReportStyle {
Expand Down Expand Up @@ -407,6 +408,7 @@ fn lint_one_input(
weirpack_inputs: _,
color: _,
format: _,
quiet: _,
} = lint_options;

let mut lint_kinds: HashMap<LintKind, usize> = HashMap::new();
Expand Down Expand Up @@ -526,7 +528,7 @@ fn lint_one_input(
&lint_rules,
// Reporting arguments
batch_mode,
report_mode,
(report_mode, lint_options.quiet),
);
}
}
Expand Down Expand Up @@ -629,8 +631,10 @@ fn single_input_report(
lint_rules: &HashMap<String, usize>,
// Reporting parameters
batch_mode: bool, // If true, we are processing multiple files, which affects how we report
report_mode: &ReportStyle,
report_info: (&ReportStyle, bool),
) {
let (report_mode, quiet) = report_info;

// JSON mode: all output is handled by the caller after collecting results
if matches!(report_mode, ReportStyle::Json) {
return;
Expand Down Expand Up @@ -667,23 +671,30 @@ fn single_input_report(

if batch_mode && longest > MAX_LINE_LEN && matches!(report_mode, ReportStyle::FullAriadne) {
report_mode = &ReportStyle::BriefCountsOnly;
println!(
"{}: Longest line: {longest} exceeds max line length: {MAX_LINE_LEN}",
input.format_path()
);
if !quiet {
println!(
"{}: Longest line: {longest} exceeds max line length: {MAX_LINE_LEN}",
input.format_path()
);
}
}

// Report the number of lints no matter what report mode we are in
println!(
"{}: {}",
input.format_path(),
match (lint_count_before, lint_count_after) {
(0, _) => "No lints found".to_string(),
(before, after) if before != after =>
format!("{before} lints before overlap removal, {after} after"),
(before, _) => format!("{before} lints"),
if lint_count_before == 0 {
if !quiet {
println!("{}: No lints found", input.format_path());
}
);
} else {
println!(
"{}: {}",
input.format_path(),
match (lint_count_before, lint_count_after) {
(before, after) if before != after =>
format!("{before} lints before overlap removal, {after} after"),
(before, _) => format!("{before} lints"),
}
);
}

// If we are in Ariadne mode, print the report
if matches!(report_mode, ReportStyle::FullAriadne) {
Expand Down
5 changes: 5 additions & 0 deletions harper-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ enum Args {
/// Output format for lint results.
#[arg(long, value_enum, default_value_t = OutputFormat::Default)]
format: OutputFormat,
/// Suppress informational status messages and only output actual lint errors.
#[arg(long)]
quiet: bool,
},
/// Parse a provided document and print the detected symbols.
Parse {
Expand Down Expand Up @@ -251,6 +254,7 @@ fn main() -> anyhow::Result<()> {
file_dict_path,
weirpacks,
format,
quiet,
} => {
let dialect = parse_dialect(&dialect_str)
.map_err(|e| anyhow!("Invalid dialect '{}': {}", dialect_str, e))?;
Expand All @@ -268,6 +272,7 @@ fn main() -> anyhow::Result<()> {
weirpack_inputs: weirpacks,
color,
format,
quiet,
},
user_dict_path,
// TODO workspace_dict_path?
Expand Down
6 changes: 6 additions & 0 deletions harper-core/default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4997,6 +4997,12 @@
"state": true,
"label": "Pay For Price"
}
},{
"Bool": {
"name": "NumericModifier",
"state": true,
"label": "Numeric Modifier"
}
}
]
}
Expand Down
2 changes: 1 addition & 1 deletion harper-core/dictionary.dict
Original file line number Diff line number Diff line change
Expand Up @@ -50839,7 +50839,7 @@ ugh/~
uglification/NwgS
ugliness/Nmg
ugly/~J^>pNV
uh/~N
uh/~
uh-huh
uh-oh
uhf/~
Expand Down
2 changes: 2 additions & 0 deletions harper-core/src/linting/lint_group/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ use super::nor_modal_pronoun::NorModalPronoun;
use super::not_only_inversion::NotOnlyInversion;
use super::noun_verb_confusion::NounVerbConfusion;
use super::number_suffix_capitalization::NumberSuffixCapitalization;
use super::numeric_modifier::NumericModifier;
use super::numeric_range_en_dash::NumericRangeEnDash;
use super::obsess_preposition::ObsessPreposition;
use super::of_course::OfCourse;
Expand Down Expand Up @@ -713,6 +714,7 @@ impl LintGroup {
insert_expr_rule!(NotOnlyInversion, true);
insert_struct_rule!(NounVerbConfusion, true);
insert_struct_rule!(NumberSuffixCapitalization, true);
insert_struct_rule!(NumericModifier, true);
insert_expr_rule!(NumericRangeEnDash, true);
insert_expr_rule!(ObsessPreposition, true);
insert_expr_rule!(OfCourse, true);
Expand Down
1 change: 1 addition & 0 deletions harper-core/src/linting/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ mod nor_modal_pronoun;
mod not_only_inversion;
mod noun_verb_confusion;
mod number_suffix_capitalization;
mod numeric_modifier;
mod numeric_range_en_dash;
mod obsess_preposition;
mod of_course;
Expand Down
223 changes: 223 additions & 0 deletions harper-core/src/linting/numeric_modifier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
use crate::{
Lint, Token, TokenKind, TokenStringExt,
expr::{All, Expr, SequenceExpr, SpelledNumberExpr},
linting::{ExprLinter, LintKind, debug::format_lint_match, expr_linter::Chunk},
patterns::WordSet,
};

pub struct NumericModifier {
expr: SequenceExpr,
}

const PLURAL_UNITS: &str = "acres|amperes|amps|bars|bits|bowls|bucks|bytes|\
cents|cms|centimeters|centmetres|centuries|colors|\
colours|columns|cups|days|decades|decibels|\
deciliters|decilitres|decimeters|decimetres|degs|\
degrees|dollars|drops|ems|euros|feet|frames|gals|\
gallons|gigabits|gigabytes|gigs|glasses|handfuls|\
hectares|hrs|hours|inches|joules|keys|kilobits|\
kilobytes|kgs|kilograms|kilojoules|kilometers|\
kilometres|kilopascals|lightyears|lbs|megs|lines|\
liters|megabits|megabytes|meters|metres|mis|miles|\
mins|minutes|milligrams|mls|milliliters|\
millilitres|millimeters|millimetres|mos|months|\
megapascals|nanometers|nanometres|ohms|ounces|\
pages|parsecs|paragraphs|pascals|pence|petabits|\
petabytes|pixels|pounds|quarters|qts|quarts|rads|\
radians|rows|seasons|secs|seconds|slices|stages|\
strings|terabits|terabytes|ticks|tonnes|tons|volts|\
watts|wks|weeks|words|yds|yards|yrs|years";
impl Default for NumericModifier {
fn default() -> Self {
let number = SequenceExpr::any_of(vec![
Box::new(SpelledNumberExpr),
Box::new(SequenceExpr::default().then_cardinal_number()),
]);

// e.g. "10 ‹YEARS› old laptop"
let unit = All::new(vec![
Box::new(WordSet::new(PLURAL_UNITS.split('|'))),
Box::new(|t: &Token, s: &[char]| t.kind.is_plural_noun() && s.len() > 1),
]);

// e.g. "10 years ‹OLD› laptop"
let dimensional_adjective = SequenceExpr::default().then_kind_is_but_isnt_any_of_except(
TokenKind::is_adjective,
&[
TokenKind::is_superlative_adjective,
TokenKind::is_preposition,
TokenKind::is_verb_progressive_form,
],
&["it", "left", "max", "now", "paid", "per", "spent"],
);

// e.g. "10 years old ‹LAPTOP›"
let noun = SequenceExpr::default().then_kind_is_but_isnt_any_of_except(
TokenKind::is_noun,
&[TokenKind::is_conjunction, TokenKind::is_preposition],
&["uh"],
);

Self {
expr: number
.t_ws_h()
.then(unit)
.then_optional(SequenceExpr::default().t_ws_h().then(dimensional_adjective))
.t_ws()
.then(noun),
}
}
}

impl ExprLinter for NumericModifier {
type Unit = Chunk;

fn match_to_lint_with_context(
&self,
matched_tokens: &[Token],
source: &[char],
context: Option<(&[Token], &[Token])>,
) -> Option<Lint> {
eprintln!("🚨 {}", format_lint_match(matched_tokens, context, source));
let span = matched_tokens.span()?;
Some(Lint {
span,
lint_kind: LintKind::Miscellaneous,
suggestions: vec![],
message: "👎".to_string(),
..Default::default()
})
}

fn expr(&self) -> &dyn Expr {
&self.expr
}

fn description(&self) -> &str {
"Flags plural units used in modifiers."
}
}

#[cfg(test)]
mod tests {
use crate::linting::tests::assert_suggestion_result;

use super::NumericModifier;

#[test]
fn arguments_function() {
assert_suggestion_result(
"The Ackerman function is a two arguments function.",
NumericModifier::default(),
"The Ackerman function is a two argument function.",
);
}

#[test]
fn bytes_sequence() {
assert_suggestion_result(
"My laptop is a six years old Lenovo Ideapad L340-17IRH Gaming.",
NumericModifier::default(),
"My laptop is a six year old Lenovo Ideapad L340-17IRH Gaming.",
);
}

#[test]
fn columns_dataframe() {
assert_suggestion_result(
"Building a tree from a two columns data frame",
NumericModifier::default(),
"Building a tree from a two column data frame",
);
}

#[test]
fn days_event() {
assert_suggestion_result(
"That is, is 1/1/2018-2/1/2018 a two days event, or a 1 day event like in ical",
NumericModifier::default(),
"That is, is 1/1/2018-2/1/2018 a two day event, or a 1 day event like in ical",
);
}

#[test]
fn days_window() {
assert_suggestion_result(
"how much the customer paid in a seven days window",
NumericModifier::default(),
"how much the customer paid in a seven day window",
);
}

#[test]
fn dollars_tip() {
assert_suggestion_result(
"A two-dollars tip each day!",
NumericModifier::default(),
"A two-dollar tip each day!",
);
}

#[test]
fn feet_pole() {
assert_suggestion_result(
"I know someone who won't even touch six words with a ten-feet pole",
NumericModifier::default(),
"I know someone who won't even touch six words with a ten-foot pole",
);
}

#[test]
fn lines_program() {
assert_suggestion_result(
"Adding a two lines program that triggers the issue here",
NumericModifier::default(),
"Adding a two line program that triggers the issue here",
);
}

#[test]
fn meters_distance() {
assert_suggestion_result(
"in front of the sensor (at a three meters distance)",
NumericModifier::default(),
"in front of the sensor (at a three meter distance)",
);
}

#[test]
fn minutes_walk() {
assert_suggestion_result(
"Take a Ten Minutes Walk.",
NumericModifier::default(),
"Take a Ten Minute Walk.",
);
}

#[test]
fn parameters_function() {
assert_suggestion_result(
"I need to implement a two parameters function with python.",
NumericModifier::default(),
"I need to implement a two parameter function with python.",
);
}

#[test]
fn params_function() {
assert_suggestion_result(
"This is an alias over zip and then apply a two params function.",
NumericModifier::default(),
"This is an alias over zip and then apply a two param function.",
);
}

#[test]
fn years_old() {
assert_suggestion_result(
"My laptop is a six years old Lenovo Ideapad L340-17IRH Gaming.",
NumericModifier::default(),
"My laptop is a six year old Lenovo Ideapad L340-17IRH Gaming.",
);
}
}
2 changes: 1 addition & 1 deletion harper-core/src/linting/web_scraping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl Default for WebScraping {
let scrap_verbs = &["scrap", "scrapped", "scraps", "scrapping"][..];
let scrap_nouns = &["scrapper", "scrappers"][..];

let mut closed_compounds = WordSet::new(&[]);
let mut closed_compounds = WordSet::new(std::iter::empty::<&str>());
let mut open_and_hyphenated_compounds = vec![];

scrap_verbs.iter().chain(scrap_nouns).for_each(|scrap| {
Expand Down
4 changes: 2 additions & 2 deletions harper-core/src/patterns/word_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ impl WordSet {
}

/// Create a new word set that matches against any word in the provided list.
pub fn new(words: &[&'static str]) -> Self {
pub fn new(words: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let mut set = Self::default();

for str in words {
set.add(str);
set.add(str.as_ref());
}

set
Expand Down
Loading