fix(core): flag possessive "it's" after "has" and "had" - #4035
fix(core): flag possessive "it's" after "has" and "had"#4035mauropereiira wants to merge 1 commit into
Conversation
`ItsPossessive`'s mid-sentence expression required the token before `it's` to tag as VERB or ADP. Harper's Brill tagger labels possessive "has" and "had" as AUX, so the entire "has it's <adjective> <noun>" class was never matched, including the reporter's "but has it's own stack". Adding the two words to the leading token set fixes the class. Copular "is"/"was" are deliberately excluded, since "The problem is it's poor design." is correct as written and must not be flagged. Fixes Automattic#3627 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hippietrail
left a comment
There was a problem hiding this comment.
Just sharing a thought about UPOS vs TokenKind.
| // VERB, so they are not covered by the sets above. Other forms ("have", | ||
| // "hasn't", "hadn't") already tag as VERB and need no special handling. | ||
| let mid_sentence_lead = | ||
| UPOSSet::new(&[UPOS::VERB, UPOS::ADP]).or(SequenceExpr::word_set(&["has", "had"])); |
There was a problem hiding this comment.
I never got into UPOS but in the regular DictWordMetadata via TokenKind, auxiliary verb is a property on verb so you can use .is_auxiliary_verb() or just .is_verb(), which seems cleaner to me than special-casing "has" and "had" but I'm not really sure here. Might be worth trying out?
There was a problem hiding this comment.
Good question, and it sent me down a useful path. I tried it and the answer is that TokenKind does not work here, but the reason is more interesting than I expected.
First, .is_auxiliary_verb() specifically will not do it. has and had are not tagged as auxiliary in the curated dictionary, only have and be are:
WORD verb auxiliary linking
has true not set not set
had true not set not set
have true true not set
is true not set true
was true not set true
So that predicate misses exactly the two words this PR is about.
.is_verb() does cover them, and I had to exclude linking verbs to keep the copula guard, so I built and tested this version:
let mid_sentence_lead = UPOSSet::new(&[UPOS::VERB, UPOS::ADP])
.or(|tok: &Token, _: &[char]| tok.kind.is_verb() && !tok.kind.is_linking_verb());It compiles, passes all 6125 tests with no snapshot changes, and behaves identically to the current version on every sentence in this PR, including the copula cases. It looks like a clean win right up until you try inputs no test covers:
current is_verb()
In the end it's good news. ok FLAG
The water it's clear surface. ok FLAG
The report it's main findings. ok FLAG
The process it's own stack. ok FLAG
The design it's poor quality. ok FLAG
That is the crux of your UPOS versus TokenKind question. TokenKind::is_verb() is a dictionary lookup asking whether the word has a verb sense anywhere, with no disambiguation. UPOS is what the tagger decided for this token in this sentence. So every noun that also happens to be a verb, and English has a lot of them, becomes a valid lead token.
So I am leaving the PR as it is. The two-word set is not elegant, but it is scoped to a known tagger gap rather than trading a precise signal for a fuzzy one. Happy to be talked out of it if you see a third option I have missed.
Incidentally, two of my probes fail on both versions, so they are pre-existing rather than anything this PR introduces:
At the start it's hard work. FLAG
After the show it's late night. FLAG
Those look like the same family as #4038.
There was a problem hiding this comment.
First,
.is_auxiliary_verb()specifically will not do it.hasandhadare not tagged as auxiliary in the curated dictionary, onlyhaveandbeare:
You're right! In dictionary.dict only some of them have the /A flag and I didn't check all the negative contractions either.
This is its own bug that needs to be fixed!
One problem is that "auxiliary verb" is a somewhat fuzzy term and not all sources put the same things in it. I think I have a tab open with research I was doing on this very question still open... Hmm no I must've cleaned up excessive "open for later use" tabs.
There was a problem hiding this comment.
Agreed, and I audited it so the data is written down somewhere. Opened #4086.
Short version: the modals are the one clean group, all nine carry the flag. The rest is not.
be A have A do A
am - has - does A
is - had - did A
are - having - doing -
was - done -
were -
being -
been -
The negative contractions split down the middle too, with isn't flagged but aren't, wasn't and weren't not, and haven't flagged but hasn't and hadn't not.
Also trojan/NgSVAdG carries it, which came in with #2202. best and better do as well, though better is at least arguable through the "had better" construction.
On your fuzziness point, I deliberately have not turned the missing list into a patch. Where the boundary sits for the semi-modals and for being/been/doing/done decides which of those gaps are real errors, so it wants a definition first. Happy to do the dictionary pass once there is a call on it.
None of this changes this PR, the two-word set stands regardless.
Issues
Fixes #3627
Description
ItsPossessive's mid-sentence expression required the token immediately beforeit'sto tag asVERBorADP:Harper's Brill tagger labels possessive "has" and "had" as
AUX, notVERB. So the wholehas it's <adjective> <noun>class was never matched, which is why the reporter's "but has it's own stack" went unflagged.The word "own" turned out to be a red herring. It tags as
ADJand already matched fine after a true verb. On master, before this patch:Only the preceding word differs between those pairs, so the gap is the tag on the verb, not anything about the noun phrase.
This adds "has" and "had" to the leading token set. I deliberately did not add
UPOS::AUXwholesale, because the copula also tagsAUX, and that would introduce false positives on sentences like "The problem is it's poor design.", whereit'sgenuinely means "it is". Both of those cases are covered by new regression tests.I also checked "have", "hasn't" and "hadn't": they already tag as
VERBin this position and match without any change, so they are not in the set.Scope note: this touches one file and does not modify
default_config.json, sinceItsPossessiveis already registered and enabled.Demo
Not applicable, this is a linter rule change. CLI output is below.
How Has This Been Tested?
cargo test -p harper-core— 6125 + 41 tests pass, 0 failures, no snapshot changes.Manually with
cargo run --bin harper-cli --release -- lint, comparing a build ofmasteragainst this branch.Now flagged, previously silent:
Still clean, regression guards:
Unchanged, paths that already worked:
AI Disclosure
If Your PR Implements or Enhances a Linter
The "has it's own stack" case comes directly from the issue. The remaining sentences are minimal variations built to isolate the
AUXvsVERBdistinction and to pin the copular false positives.Checklist
Related but deliberately excluded, happy to open these separately:
ItsPossessivedoes not match typographic apostrophes.The project drifted from it’s goal.is silent, while the straight-quote version flags. That is issue False negative:from it’s goalnot flagged to correct tofrom its goal#3202, and the cause is different:t_acois backed byWord, which compares chars directly, whereasWordSetnormalises the apostrophe (see thesupports_typographic_apostrophestest inword_set.rs). Worth deciding whetherWordshould normalise too, since that would affect many linters, so I did not touch it here.I have heard it's good news.flags on master today, whereit'scorrectly means "it is".