Skip to content

fix(linting): stop flagging the adjective "leftover" as a phrasal verb#3792

Open
AnayGarodia wants to merge 3 commits into
Automattic:masterfrom
AnayGarodia:fix/leftover-adjective-fp
Open

fix(linting): stop flagging the adjective "leftover" as a phrasal verb#3792
AnayGarodia wants to merge 3 commits into
Automattic:masterfrom
AnayGarodia:fix/leftover-adjective-fp

Conversation

@AnayGarodia

Copy link
Copy Markdown

Disclaimer: This PR was produced with LLM assistance, reviewed and verified by me before submission (per Harper's agent-PR policy).

What

Fixes #3707. PhrasalVerbAsCompoundNoun flagged the adjective "leftover" ("leftover debris") and suggested rewriting it to "left over".

Why

"leftover" ends in the particle "over" and its prefix "left" is a verb, so it matches the rule's phrasal-verb shape. But used attributively it's an adjective, not a phrasal verb miswritten as a compound noun. This is the exact false-positive class the rule already handles for "gallon" ("gall on") and "dragon" ("drag on").

How

Add leftover to the existing known-false-positive set in analyse, and update the accompanying comment. A blanket "skip adjectives" approach was rejected because "backup" is also tagged as an adjective, and skipping it would regress the rule's intended backupback up corrections (verified locally — 2 existing tests fail with that approach).

Testing

Added dont_flag_leftover_adjective_3707 (reproduces the issue: 1 lint before, 0 after). Full rule suite green, no regressions:

cargo test -p harper-core --lib phrasal_verb_as_compound_noun   # 65 passed, 1 ignored
cargo fmt -- --check                                            # clean
cargo clippy -- -Dwarnings ...                                  # clean

Compatibility

Purely narrows one false positive; no public API or output change for any other input.

Copilot AI review requested due to automatic review settings July 8, 2026 21:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@hippietrail hippietrail left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fix does not fit the pattern where it was added.

if noun_chars.eq_any_ignore_ascii_case_chars(&[
&['g', 'a', 'l', 'l', 'o', 'n'],
&['d', 'r', 'a', 'g', 'o', 'n'],
&['l', 'e', 'f', 't', 'o', 'v', 'e', 'r'],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not appropriate. "Gallon" and "dragon" merely look like compound nouns that might be related to phrasal verbs but "leftover" is an actual compound noun that is related to an actual phrasal verb 'left over".

A different fix will have to be found. I highly recommend a data-based approach using as many actual mistakes and as many legit uses as you can find. AI coding agents so far seem to be terrible at this and only make up examples that fit their assumptions. I think they can't freely use Google, which is how I find examples and counterexamples when working on new linters.

@AnayGarodia

Copy link
Copy Markdown
Author

you're right, that didn't fit. gallon/dragon are in the list because they're coincidental splits — "gall on"/"drag on" only look like verb+particle. "leftover" is a genuine one ("left over"), so hardcoding it was papering over the real reason.

reworked it: dropped "leftover" from the list and added a contextual guard — when there's no usable previous word (punctuation before it) and the word is itself an adjective sitting right before a noun, it's attributive ("passive, leftover debris") rather than a phrasal verb written as a compound noun. scoped to the no-previous-word case so it doesn't touch "to backup individual apps" and friends, which the existing prev-word guards already handle. generalizes past the one word, no corpus snapshot changes.

@hippietrail hippietrail left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More gotchas to look for I'm afraid. There are many subtleties in grammar checking due to words beloning in multiple POS categories.

// is a previous word the adjective/determiner/preposition guards below decide.
if maybe_prev_tok.is_none()
&& token.kind.is_adjective()
&& maybe_next_tok.is_some_and(|t| t.kind.is_noun())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another common gotcha is that many words are both nouns and adjectives, so you would want to find examples where the first such is in both categories and where the second such is in both categories.

Then you'd want to test with the first token being an adjective and not also being a noun and the second maybe_next_tok being a noun and not also an adjective.

And then you want to test whether doing so doesn't rule out too many instances of the problem you were trying to fix.

This is the kind of lint that having a big set of real-world text files or markdown files works great with, by the way.

@AnayGarodia

Copy link
Copy Markdown
Author

ok, redid this properly - dropped the earlier no-previous-word special case.

the thing #3707 was actually missing: "passive, leftover debris" already has an adjective ("passive") sitting right before the word, which is exactly what the existing guard at line 168 checks for. but the comma made get_next_word_from_offset return None, so that guard never got to run. so rather than special-casing "leftover" i made it look back past the punctuation to find that preceding adjective/determiner. same guard you already had, just no longer blocked by a comma.

kept it tight: the previous word (across punctuation) has to be an adjective or determiner, and the following word has to be a noun. so imperative errors still flag - "Backup your database before upgrading" -> "Back up ...", and "Run fast, backup often" stays flagged because "often" is an adverb, not a noun.

on the multiple-POS point: this linter only iterates noun tokens, so the target word is always a noun - there is no adjective-that-is-not-a-noun case on that side. on the following-word side i checked both noun-only (debris, dinner rolls, database) and noun+adjective.

for examples i went looking instead of inventing them. Writer's Digest and Longman for the attributive uses ("leftover paint", "three leftover dinner rolls"), and Linguix actually lists "back up" as a documented common mistake ("They backup the files" -> "back up"), plus grammar.com ("Please backup your work" -> "back up").

one thing i want to be upfront about: "Stay calm, backup files" gets suppressed now too. it is the same shape as "passive, leftover debris" - a coordinate adjective vs two imperative clauses split by a comma - and i do not think those can be told apart without a real parse. it is narrow (needs adjective, comma, the word, then a noun) and it trades against the more common attributive false positive, with no snapshot changes either way. if you would rather not make that trade i am happy to drop the approach.

pratyushsinghal7 and others added 3 commits July 17, 2026 12:31
PhrasalVerbAsCompoundNoun split "leftover" into "left over" because it
ends in the particle "over" and "left" is a verb. But "leftover" (as in
"leftover debris") is an adjective, not a phrasal verb miswritten as a
compound noun — the same false-positive shape already handled for
"gallon" (gall on) and "dragon" (drag on). Add it to the known
false-positive set and cover it with a test.

Closes Automattic#3707.
… leftover

drop "leftover" from the known-false-positive list - unlike gallon/dragon
(coincidental verb+particle splits) it's a real adjective used attributively.
when there's no usable previous word (punctuation before it) and the word is
an adjective right before a noun, treat it as attributive ("passive, leftover
debris"). generalizes past the single word. refs Automattic#3707.
The Automattic#3707 case "passive, leftover debris" already has an adjective right
before the word, which the existing attributive/determiner guard checks
for. The comma made get_next_word_from_offset return None, so that guard
never ran. Walk back across punctuation to find the modifier instead of
special-casing the word.

Kept narrow: the preceding word (past punctuation) must be an adjective
or determiner and the following word must be a noun, so imperative errors
still flag ("Backup your database ..." -> "Back up ...", "Run fast, backup
often" stays flagged since "often" is not a noun).

No corpus snapshot changes.
@AnayGarodia
AnayGarodia force-pushed the fix/leftover-adjective-fp branch from c1056bf to 3ed422c Compare July 17, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

False positive: adjective "leftover" flagged to change to "left over"

4 participants