eslint-plugin-harlanzw 0.16.0 → 0.17.0

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.
Files changed (3) hide show
  1. package/README.md +2 -0
  2. package/dist/index.mjs +119 -33
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -76,6 +76,7 @@ The rules are organized into the following categories:
76
76
  | [`ai-deslop-buzzwords`](./src/prompt/rules/deslop-buzzwords.ts) | replace AI-generated buzzword phrases with simpler alternatives (e.g. "leverage" → "use") |
77
77
  | [`ai-deslop-casing`](./src/prompt/rules/deslop-casing.ts) | enforce correct casing for tech terms, brands, and abbreviations (e.g. "github" → "GitHub") |
78
78
  | [`ai-deslop-false-dichotomy`](./src/prompt/rules/deslop-false-dichotomy.ts) | flag "it's not X, it's Y" contrast patterns common in AI writing |
79
+ | [`ai-deslop-false-sincerity`](./src/prompt/rules/deslop-false-sincerity.ts) | remove false-sincerity openers that pad sentences ("honestly", "frankly", "in all honesty") |
79
80
  | [`ai-deslop-filler`](./src/prompt/rules/deslop-filler.ts) | remove AI-generated filler sentences and phrases (e.g. "it's worth noting that") |
80
81
  | [`ai-deslop-hedging`](./src/prompt/rules/deslop-hedging.ts) | remove hedging/qualifying words that weaken copy (e.g. "very", "really", "quite", "just") |
81
82
  | [`ai-deslop-no-em-dash`](./src/prompt/rules/deslop-no-em-dash.ts) | replace em dashes in content prose |
@@ -211,6 +212,7 @@ export default [
211
212
  | `ai-deslop-casing` | Fixes tech term casing using a 300+ term dictionary ("github" → "GitHub", "typescript" → "TypeScript") |
212
213
  | `ai-deslop-autolink` | Links first mention of tech terms to their canonical URLs ("Nuxt" → `[Nuxt](https://nuxt.com)`) |
213
214
  | `ai-deslop-false-dichotomy` | Flags "it's not X, it's Y" false contrast patterns |
215
+ | `ai-deslop-false-sincerity` | Strips false-sincerity openers ("honestly", "frankly", "in all honesty", "let's be real") |
214
216
  | `ai-deslop-hedging` | Strips hedging words that weaken copy ("very", "really", "quite", "just", "somewhat") |
215
217
  | `ai-deslop-no-em-dash` | Replaces em dashes in content prose |
216
218
  | `ai-deslop-no-exclamation` | Replaces exclamation marks with periods in content prose |
package/dist/index.mjs CHANGED
@@ -4,7 +4,7 @@ import process from 'node:process';
4
4
  import { TextSourceCodeBase, ConfigCommentParser, Directive, VisitNodeStep } from '@eslint/plugin-kit';
5
5
  import { AST_NODE_TYPES } from '@typescript-eslint/utils';
6
6
 
7
- const version = "0.16.0";
7
+ const version = "0.17.0";
8
8
 
9
9
  const STRENGTH_PATTERNS = {
10
10
  strong: ["never", "must", "always", "under no circumstances", "absolutely", "required", "mandatory", "forbidden", "prohibited"],
@@ -356,6 +356,17 @@ const FILLER_PHRASES = [
356
356
  "to summarize",
357
357
  "to sum up"
358
358
  ];
359
+ const FALSE_SINCERITY_OPENERS = [
360
+ "honestly",
361
+ "frankly",
362
+ "in all honesty",
363
+ "in all fairness",
364
+ "to be frank",
365
+ "let's be real",
366
+ "if i'm honest",
367
+ "if i'm being honest",
368
+ "if we're being honest"
369
+ ];
359
370
  const UNNECESSARY_ADVERBS = [
360
371
  "fundamentally",
361
372
  "essentially",
@@ -718,7 +729,7 @@ function parseFrontmatter(lines) {
718
729
  }
719
730
 
720
731
  const REGEX_1$u = /[.*+?^${}()|[\]\\]/g;
721
- const COMPILED$8 = AMBIGUOUS_QUANTIFIERS.map((quantifier) => {
732
+ const COMPILED$9 = AMBIGUOUS_QUANTIFIERS.map((quantifier) => {
722
733
  const escaped = quantifier.replace(REGEX_1$u, "\\$&");
723
734
  return new RegExp(`\\b${escaped}\\b`, "gi");
724
735
  });
@@ -743,8 +754,8 @@ const promptAmbiguousQuantifier = {
743
754
  if (shouldSkipLine(i, codeBlockLines, frontmatterEnd))
744
755
  continue;
745
756
  const line = lines[i];
746
- for (let qi = 0; qi < COMPILED$8.length; qi++) {
747
- const regex = COMPILED$8[qi];
757
+ for (let qi = 0; qi < COMPILED$9.length; qi++) {
758
+ const regex = COMPILED$9[qi];
748
759
  regex.lastIndex = 0;
749
760
  let match;
750
761
  while ((match = regex.exec(line)) !== null) {
@@ -773,7 +784,7 @@ const promptAmbiguousQuantifier = {
773
784
 
774
785
  const REGEX_2$d = /^[-*>\s#\d.]+$/;
775
786
  const REGEX_1$t = /\.\s+$/;
776
- const COMPILED$7 = UNNECESSARY_ADVERBS.map((adverb) => {
787
+ const COMPILED$8 = UNNECESSARY_ADVERBS.map((adverb) => {
777
788
  return { regex: new RegExp(`\\b${adverb}\\s+`, "gi"), adverb };
778
789
  });
779
790
  const aiDeslopAdverbs = {
@@ -799,7 +810,7 @@ const aiDeslopAdverbs = {
799
810
  const line = lines[i];
800
811
  const lineNode = node.children[i];
801
812
  const scopes = parseLineScopes(line);
802
- for (const { regex, adverb } of COMPILED$7) {
813
+ for (const { regex, adverb } of COMPILED$8) {
803
814
  regex.lastIndex = 0;
804
815
  let match;
805
816
  while ((match = regex.exec(line)) !== null) {
@@ -940,7 +951,7 @@ const DEDUPED_ENTRIES = SORTED_ENTRIES$1.filter(([name, url]) => {
940
951
  SEEN_URLS.set(url, name);
941
952
  return true;
942
953
  });
943
- const COMPILED$6 = DEDUPED_ENTRIES.map(([name, url]) => {
954
+ const COMPILED$7 = DEDUPED_ENTRIES.map(([name, url]) => {
944
955
  const escaped = name.replace(REGEX_3$7, "\\$&");
945
956
  return { name, url, regex: new RegExp(`(?<=\\s|^)${escaped}(?=\\s|$|[.,;:!?)])`, "g") };
946
957
  });
@@ -963,7 +974,7 @@ const aiDeslopAutolink = {
963
974
  const codeBlockLines = getCodeBlockLines(lines);
964
975
  const frontmatterEnd = getFrontmatterEnd(lines);
965
976
  const linkedUrls = /* @__PURE__ */ new Set();
966
- const allUrls = new Set(COMPILED$6.map((c) => c.url));
977
+ const allUrls = new Set(COMPILED$7.map((c) => c.url));
967
978
  for (const line of lines) {
968
979
  LINK_URL_RE.lastIndex = 0;
969
980
  let urlMatch;
@@ -982,7 +993,7 @@ const aiDeslopAutolink = {
982
993
  continue;
983
994
  const lineNode = node.children[i];
984
995
  const scopes = parseLineScopes(line);
985
- for (const { name, url, regex } of COMPILED$6) {
996
+ for (const { name, url, regex } of COMPILED$7) {
986
997
  if (linkedUrls.has(url))
987
998
  continue;
988
999
  regex.lastIndex = 0;
@@ -1028,7 +1039,7 @@ const REGEX_3$6 = /[.*+?^${}()|[\]\\]/g;
1028
1039
  const REGEX_2$b = /^[-*>\s#\d.]+$/;
1029
1040
  const REGEX_1$r = /\.\s+$/;
1030
1041
  const SORTED_PHRASES = Object.entries(BUZZWORD_PHRASES).sort((a, b) => b[0].length - a[0].length);
1031
- const COMPILED$5 = SORTED_PHRASES.map(([phrase, replacement]) => {
1042
+ const COMPILED$6 = SORTED_PHRASES.map(([phrase, replacement]) => {
1032
1043
  const escaped = phrase.replace(REGEX_3$6, "\\$&");
1033
1044
  return { regex: new RegExp(`\\b${escaped}\\b`, "gi"), phrase, replacement };
1034
1045
  });
@@ -1056,7 +1067,7 @@ const aiDeslopBuzzwords = {
1056
1067
  const lineNode = node.children[i];
1057
1068
  const scopes = parseLineScopes(line);
1058
1069
  const matched = [];
1059
- for (const { regex, replacement } of COMPILED$5) {
1070
+ for (const { regex, replacement } of COMPILED$6) {
1060
1071
  regex.lastIndex = 0;
1061
1072
  let match;
1062
1073
  while ((match = regex.exec(line)) !== null) {
@@ -1433,7 +1444,7 @@ const CASING_DICTIONARY = {
1433
1444
 
1434
1445
  const REGEX_1$q = /[.*+?^${}()|[\]\\]/g;
1435
1446
  const SORTED_ENTRIES = Object.entries(CASING_DICTIONARY).sort((a, b) => b[0].length - a[0].length);
1436
- const COMPILED$4 = SORTED_ENTRIES.map(([key, correct]) => {
1447
+ const COMPILED$5 = SORTED_ENTRIES.map(([key, correct]) => {
1437
1448
  const escaped = key.replace(REGEX_1$q, "\\$&");
1438
1449
  return { regex: new RegExp(`\\b${escaped}\\b`, "gi"), correct };
1439
1450
  });
@@ -1461,7 +1472,7 @@ const aiDeslopCasing = {
1461
1472
  const lineNode = node.children[i];
1462
1473
  const scopes = parseLineScopes(line);
1463
1474
  const matched = [];
1464
- for (const { regex, correct } of COMPILED$4) {
1475
+ for (const { regex, correct } of COMPILED$5) {
1465
1476
  regex.lastIndex = 0;
1466
1477
  let match;
1467
1478
  while ((match = regex.exec(line)) !== null) {
@@ -1557,12 +1568,12 @@ const aiDeslopCodeLang = {
1557
1568
  }
1558
1569
  };
1559
1570
 
1560
- const APOSTROPHE = `(?:'|\u2019)`;
1561
- const SUBJECT_NOT = `(?:it${APOSTROPHE}s|it is|they${APOSTROPHE}re|they are|we${APOSTROPHE}re|we are|you${APOSTROPHE}re|you are|this is|that is|is|are)`;
1562
- const SUBJECT_IS = `(?:it${APOSTROPHE}s|it is|they${APOSTROPHE}re|they are|we${APOSTROPHE}re|we are|you${APOSTROPHE}re|you are|this is|that is|is|are)`;
1563
- const CONTRACTION_NOT = `(?:isn${APOSTROPHE}t|aren${APOSTROPHE}t|wasn${APOSTROPHE}t|weren${APOSTROPHE}t)`;
1564
- const NO_LONGER = `(?:is|it${APOSTROPHE}s|was)`;
1565
- const PATTERN = new RegExp(`\\b${SUBJECT_NOT}\\s+not\\b.{1,80}?\\b${SUBJECT_IS}\\b|\\b${CONTRACTION_NOT}\\s+(?:just\\s+)?about\\b.{1,80}?\\b(?:it${APOSTROPHE}s|is)\\s+about\\b|\\b${NO_LONGER}\\s+no\\s+longer\\b.{1,60}?\\b(?:it${APOSTROPHE}s|is)\\b`, "i");
1571
+ const APOSTROPHE$1 = `(?:'|\u2019)`;
1572
+ const SUBJECT_NOT = `(?:it${APOSTROPHE$1}s|it is|they${APOSTROPHE$1}re|they are|we${APOSTROPHE$1}re|we are|you${APOSTROPHE$1}re|you are|this is|that is|is|are)`;
1573
+ const SUBJECT_IS = `(?:it${APOSTROPHE$1}s|it is|they${APOSTROPHE$1}re|they are|we${APOSTROPHE$1}re|we are|you${APOSTROPHE$1}re|you are|this is|that is|is|are)`;
1574
+ const CONTRACTION_NOT = `(?:isn${APOSTROPHE$1}t|aren${APOSTROPHE$1}t|wasn${APOSTROPHE$1}t|weren${APOSTROPHE$1}t)`;
1575
+ const NO_LONGER = `(?:is|it${APOSTROPHE$1}s|was)`;
1576
+ const PATTERN = new RegExp(`\\b${SUBJECT_NOT}\\s+not\\b.{1,80}?\\b${SUBJECT_IS}\\b|\\b${CONTRACTION_NOT}\\s+(?:just\\s+)?about\\b.{1,80}?\\b(?:it${APOSTROPHE$1}s|is)\\s+about\\b|\\b${NO_LONGER}\\s+no\\s+longer\\b.{1,60}?\\b(?:it${APOSTROPHE$1}s|is)\\b`, "i");
1566
1577
  const aiDeslopFalseDichotomy = {
1567
1578
  meta: {
1568
1579
  type: "suggestion",
@@ -1604,6 +1615,78 @@ const aiDeslopFalseDichotomy = {
1604
1615
  }
1605
1616
  };
1606
1617
 
1618
+ const REGEX_ESCAPE = /[.*+?^${}()|[\]\\]/g;
1619
+ const APOSTROPHE = /\\'/g;
1620
+ const SENTENCE_START_PREFIX = /^[-*>\s#\d.]+$/;
1621
+ const SENTENCE_END = /[.:!?]\s+$/;
1622
+ const COMPILED$4 = FALSE_SINCERITY_OPENERS.map((opener) => {
1623
+ const escaped = opener.replace(REGEX_ESCAPE, "\\$&").replace(APOSTROPHE, `['\u2019]`);
1624
+ return { regex: new RegExp(`\\b${escaped}\\b\\s*,?\\s*`, "gi"), opener };
1625
+ });
1626
+ const aiDeslopFalseSincerity = {
1627
+ meta: {
1628
+ type: "suggestion",
1629
+ docs: { description: "Remove false-sincerity openers that pad the sentence without adding meaning" },
1630
+ fixable: "code",
1631
+ schema: [],
1632
+ messages: {
1633
+ falseSincerity: 'False-sincerity opener: "{{found}}". It signals nothing. State the point.'
1634
+ }
1635
+ },
1636
+ create(context) {
1637
+ return {
1638
+ document(node) {
1639
+ const sourceCode = context.sourceCode;
1640
+ const lines = sourceCode.lines;
1641
+ const codeBlockLines = getCodeBlockLines(lines);
1642
+ const frontmatterEnd = getFrontmatterEnd(lines);
1643
+ for (let i = 0; i < lines.length; i++) {
1644
+ if (shouldSkipLine(i, codeBlockLines, frontmatterEnd))
1645
+ continue;
1646
+ const line = lines[i];
1647
+ const lineNode = node.children[i];
1648
+ const scopes = parseLineScopes(line);
1649
+ for (const { regex, opener } of COMPILED$4) {
1650
+ regex.lastIndex = 0;
1651
+ let match;
1652
+ while ((match = regex.exec(line)) !== null) {
1653
+ const textBefore = line.slice(0, match.index);
1654
+ const isAtSentenceStart = match.index === 0 || SENTENCE_START_PREFIX.test(textBefore) || SENTENCE_END.test(textBefore);
1655
+ if (!isAtSentenceStart)
1656
+ continue;
1657
+ if (isInsideCompoundIdentifier(line, match.index, match.index + match[0].length))
1658
+ continue;
1659
+ if (isInScope(scopes, match.index, match.index + match[0].length, ["code", "link-url"]))
1660
+ continue;
1661
+ const startOffset = lineNode.position.start.offset + match.index;
1662
+ const endOffset = startOffset + match[0].length;
1663
+ const afterMatch = line.slice(match.index + match[0].length);
1664
+ context.report({
1665
+ loc: {
1666
+ start: { line: i + 1, column: match.index + 1 },
1667
+ end: { line: i + 1, column: match.index + match[0].length + 1 }
1668
+ },
1669
+ messageId: "falseSincerity",
1670
+ data: { found: opener },
1671
+ fix(fixer) {
1672
+ const nextChar = afterMatch[0];
1673
+ if (nextChar >= "a" && nextChar <= "z") {
1674
+ return [
1675
+ fixer.replaceTextRange([startOffset, endOffset], ""),
1676
+ fixer.replaceTextRange([endOffset, endOffset + 1], nextChar.toUpperCase())
1677
+ ];
1678
+ }
1679
+ return fixer.replaceTextRange([startOffset, endOffset], "");
1680
+ }
1681
+ });
1682
+ }
1683
+ }
1684
+ }
1685
+ }
1686
+ };
1687
+ }
1688
+ };
1689
+
1607
1690
  const REGEX_3$5 = /[.*+?^${}()|[\]\\]/g;
1608
1691
  const REGEX_2$a = /^[-*>\s#\d.]+$/;
1609
1692
  const REGEX_1$p = /\.\s+$/;
@@ -4963,18 +5046,18 @@ const SIDE_EFFECT_PATTERNS = /* @__PURE__ */ new Set([
4963
5046
  "requestAnimationFrame",
4964
5047
  "requestIdleCallback"
4965
5048
  ]);
4966
- const RECEIVER_SCOPED_METHODS = {
5049
+ const RECEIVER_SCOPED_METHODS = /* @__PURE__ */ new Map([
4967
5050
  // Navigation — only on router instances
4968
- push: /* @__PURE__ */ new Set(["router", "$router"]),
4969
- replace: /* @__PURE__ */ new Set(["router", "$router"]),
4970
- go: /* @__PURE__ */ new Set(["router", "$router"]),
4971
- back: /* @__PURE__ */ new Set(["router", "$router"]),
4972
- forward: /* @__PURE__ */ new Set(["router", "$router"]),
5051
+ ["push", /* @__PURE__ */ new Set(["router", "$router"])],
5052
+ ["replace", /* @__PURE__ */ new Set(["router", "$router"])],
5053
+ ["go", /* @__PURE__ */ new Set(["router", "$router"])],
5054
+ ["back", /* @__PURE__ */ new Set(["router", "$router"])],
5055
+ ["forward", /* @__PURE__ */ new Set(["router", "$router"])],
4973
5056
  // Analytics — only on analytics/tracking objects
4974
- track: /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"]),
4975
- identify: /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"]),
4976
- page: /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"])
4977
- };
5057
+ ["track", /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"])],
5058
+ ["identify", /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"])],
5059
+ ["page", /* @__PURE__ */ new Set(["analytics", "mixpanel", "segment"])]
5060
+ ]);
4978
5061
  const SIDE_EFFECT_RECEIVERS = /* @__PURE__ */ new Set([
4979
5062
  "console",
4980
5063
  "gtag",
@@ -4999,7 +5082,7 @@ function isSideEffectInHandler(node) {
4999
5082
  const method = node.callee.property.name;
5000
5083
  if (SIDE_EFFECT_PATTERNS.has(method))
5001
5084
  return true;
5002
- const allowedReceivers = RECEIVER_SCOPED_METHODS[method];
5085
+ const allowedReceivers = RECEIVER_SCOPED_METHODS.get(method);
5003
5086
  if (allowedReceivers && node.callee.object.type === "Identifier" && allowedReceivers.has(node.callee.object.name))
5004
5087
  return true;
5005
5088
  if (node.callee.object.type === "Identifier" && SIDE_EFFECT_RECEIVERS.has(node.callee.object.name))
@@ -6993,6 +7076,7 @@ const plugin = {
6993
7076
  "ai-deslop-casing": aiDeslopCasing,
6994
7077
  "ai-deslop-code-lang": aiDeslopCodeLang,
6995
7078
  "ai-deslop-false-dichotomy": aiDeslopFalseDichotomy,
7079
+ "ai-deslop-false-sincerity": aiDeslopFalseSincerity,
6996
7080
  "ai-deslop-filler": aiDeslopFiller,
6997
7081
  "ai-deslop-frontmatter-spacing": aiDeslopFrontmatterSpacing,
6998
7082
  "ai-deslop-hedging": aiDeslopHedging,
@@ -7120,6 +7204,7 @@ const deslopRules = {
7120
7204
  "harlanzw/ai-deslop-buzzwords": "error",
7121
7205
  "harlanzw/ai-deslop-casing": "error",
7122
7206
  "harlanzw/ai-deslop-false-dichotomy": "warn",
7207
+ "harlanzw/ai-deslop-false-sincerity": "error",
7123
7208
  "harlanzw/ai-deslop-filler": "error",
7124
7209
  "harlanzw/ai-deslop-adverbs": "error",
7125
7210
  "harlanzw/ai-deslop-hedging": "warn",
@@ -7319,13 +7404,14 @@ function harlanzw(options = {}, ...extraConfigs) {
7319
7404
  }
7320
7405
  function detectFramework() {
7321
7406
  const cwd = process.cwd();
7322
- const nuxt = existsSync(resolve(cwd, "nuxt.config.ts")) || existsSync(resolve(cwd, "nuxt.config.js"));
7407
+ let nuxt = existsSync(resolve(cwd, "nuxt.config.ts")) || existsSync(resolve(cwd, "nuxt.config.js"));
7323
7408
  let vue = nuxt;
7324
- if (!vue) {
7409
+ if (!vue || !nuxt) {
7325
7410
  try {
7326
7411
  const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
7327
7412
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
7328
- vue = !!(deps.vue || deps.nuxt);
7413
+ nuxt = nuxt || !!deps.nuxt;
7414
+ vue = vue || !!(deps.vue || deps.nuxt);
7329
7415
  } catch {
7330
7416
  }
7331
7417
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "eslint-plugin-harlanzw",
3
3
  "type": "module",
4
- "version": "0.16.0",
4
+ "version": "0.17.0",
5
5
  "description": "Harlan's opinionated ESLint rules",
6
6
  "author": "Harlan Wilton <harlan@harlanzw.com>",
7
7
  "license": "MIT",
8
- "funding": "https://github.com/sponsors/harlan-zw",
8
+ "funding": "https://github.comcla/sponsors/harlan-zw",
9
9
  "homepage": "https://github.com/harlan-zw/eslint-plugin-harlanzw#readme",
10
10
  "repository": {
11
11
  "type": "git",