@bamboocss/eslint-plugin 1.44.1 → 1.45.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/dist/index.cjs +213 -44
  2. package/dist/index.mjs +213 -44
  3. package/package.json +8 -8
package/dist/index.cjs CHANGED
@@ -8,7 +8,7 @@ require("@typescript-eslint/utils/ts-eslint");
8
8
  let _bamboocss_shared = require("@bamboocss/shared");
9
9
  //#region package.json
10
10
  var name = "@bamboocss/eslint-plugin";
11
- var version = "1.44.1";
11
+ var version = "1.45.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/gajus/bamboocss/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -318,8 +318,8 @@ function isRecipeVariant(node, context) {
318
318
  }
319
319
  //#endregion
320
320
  //#region src/rules/file-not-included.ts
321
- const RULE_NAME$22 = "file-not-included";
322
- const rule$22 = createRule({
321
+ const RULE_NAME$23 = "file-not-included";
322
+ const rule$23 = createRule({
323
323
  create(context) {
324
324
  if (isValidFile(context)) return {};
325
325
  let hasReported = false;
@@ -340,11 +340,11 @@ const rule$22 = createRule({
340
340
  schema: [],
341
341
  type: "problem"
342
342
  },
343
- name: RULE_NAME$22
343
+ name: RULE_NAME$23
344
344
  });
345
345
  //#endregion
346
346
  //#region src/rules/no-config-function-in-source.ts
347
- const RULE_NAME$21 = "no-config-function-in-source";
347
+ const RULE_NAME$22 = "no-config-function-in-source";
348
348
  const CONFIG_FUNCTIONS = new Set([
349
349
  "defineConfig",
350
350
  "defineGlobalStyles",
@@ -359,7 +359,7 @@ const CONFIG_FUNCTIONS = new Set([
359
359
  "defineTokens",
360
360
  "defineUtility"
361
361
  ]);
362
- const rule$21 = createRule({
362
+ const rule$22 = createRule({
363
363
  create(context) {
364
364
  if (!hasPkgImport(context)) return {};
365
365
  if (!isValidFile(context)) return {};
@@ -399,12 +399,12 @@ const rule$21 = createRule({
399
399
  schema: [],
400
400
  type: "problem"
401
401
  },
402
- name: RULE_NAME$21
402
+ name: RULE_NAME$22
403
403
  });
404
404
  //#endregion
405
405
  //#region src/rules/no-debug.ts
406
- const RULE_NAME$20 = "no-debug";
407
- const rule$20 = createRule({
406
+ const RULE_NAME$21 = "no-debug";
407
+ const rule$21 = createRule({
408
408
  create(context) {
409
409
  const processedNodes = /* @__PURE__ */ new WeakSet();
410
410
  const checkObjectForDebug = (object) => {
@@ -473,12 +473,12 @@ const rule$20 = createRule({
473
473
  schema: [],
474
474
  type: "problem"
475
475
  },
476
- name: RULE_NAME$20
476
+ name: RULE_NAME$21
477
477
  });
478
478
  //#endregion
479
479
  //#region src/rules/no-deprecated-tokens.ts
480
- const RULE_NAME$19 = "no-deprecated-tokens";
481
- const rule$19 = createRule({
480
+ const RULE_NAME$20 = "no-deprecated-tokens";
481
+ const rule$20 = createRule({
482
482
  create(context) {
483
483
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
484
484
  const sendReport = (property, node, value) => {
@@ -532,11 +532,11 @@ const rule$19 = createRule({
532
532
  schema: [],
533
533
  type: "problem"
534
534
  },
535
- name: RULE_NAME$19
535
+ name: RULE_NAME$20
536
536
  });
537
537
  //#endregion
538
538
  //#region src/rules/no-descendant-selectors.ts
539
- const RULE_NAME$18 = "no-descendant-selectors";
539
+ const RULE_NAME$19 = "no-descendant-selectors";
540
540
  /**
541
541
  * Split a selector on combinators that are not inside brackets, parentheses or quotes.
542
542
  *
@@ -602,7 +602,7 @@ const selectorOf = (node) => {
602
602
  if (isLiteral(node) && typeof node.value === "string") return node.value;
603
603
  if (isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0]?.value.raw;
604
604
  };
605
- const rule$18 = createRule({
605
+ const rule$19 = createRule({
606
606
  create(context) {
607
607
  return { "Property[key.type!=/Identifier/][value.type=\"ObjectExpression\"]"(node) {
608
608
  const selector = selectorOf(node.key);
@@ -626,12 +626,12 @@ const rule$18 = createRule({
626
626
  schema: [],
627
627
  type: "problem"
628
628
  },
629
- name: RULE_NAME$18
629
+ name: RULE_NAME$19
630
630
  });
631
631
  //#endregion
632
632
  //#region src/rules/no-dynamic-styling.ts
633
- const RULE_NAME$17 = "no-dynamic-styling";
634
- const rule$17 = createRule({
633
+ const RULE_NAME$18 = "no-dynamic-styling";
634
+ const rule$18 = createRule({
635
635
  create(context) {
636
636
  function isStaticValue(node) {
637
637
  if (!node) return false;
@@ -702,12 +702,12 @@ const rule$17 = createRule({
702
702
  schema: [],
703
703
  type: "problem"
704
704
  },
705
- name: RULE_NAME$17
705
+ name: RULE_NAME$18
706
706
  });
707
707
  //#endregion
708
708
  //#region src/rules/no-escape-hatch.ts
709
- const RULE_NAME$16 = "no-escape-hatch";
710
- const rule$16 = createRule({
709
+ const RULE_NAME$17 = "no-escape-hatch";
710
+ const rule$17 = createRule({
711
711
  create(context) {
712
712
  const removeBrackets = (range) => {
713
713
  const [start, end] = range;
@@ -780,12 +780,12 @@ const rule$16 = createRule({
780
780
  schema: [],
781
781
  type: "problem"
782
782
  },
783
- name: RULE_NAME$16
783
+ name: RULE_NAME$17
784
784
  });
785
785
  //#endregion
786
786
  //#region src/rules/no-hardcoded-color.ts
787
- const RULE_NAME$15 = "no-hardcoded-color";
788
- const rule$15 = createRule({
787
+ const RULE_NAME$16 = "no-hardcoded-color";
788
+ const rule$16 = createRule({
789
789
  create(context) {
790
790
  const noOpacity = context.options[0]?.noOpacity;
791
791
  const whitelist = context.options[0]?.whitelist ?? [];
@@ -881,14 +881,14 @@ const rule$15 = createRule({
881
881
  }],
882
882
  type: "problem"
883
883
  },
884
- name: RULE_NAME$15
884
+ name: RULE_NAME$16
885
885
  });
886
886
  //#endregion
887
887
  //#region src/rules/no-important.ts
888
888
  const exclamationRegex = /\s*!$/;
889
889
  const importantRegex = /\s*!important\s*$/;
890
- const RULE_NAME$14 = "no-important";
891
- const rule$14 = createRule({
890
+ const RULE_NAME$15 = "no-important";
891
+ const rule$15 = createRule({
892
892
  create(context) {
893
893
  const removeQuotes = (range) => {
894
894
  const [start, end] = range;
@@ -992,12 +992,12 @@ const rule$14 = createRule({
992
992
  schema: [],
993
993
  type: "problem"
994
994
  },
995
- name: RULE_NAME$14
995
+ name: RULE_NAME$15
996
996
  });
997
997
  //#endregion
998
998
  //#region src/rules/no-invalid-nesting.ts
999
- const RULE_NAME$13 = "no-invalid-nesting";
1000
- const rule$13 = createRule({
999
+ const RULE_NAME$14 = "no-invalid-nesting";
1000
+ const rule$14 = createRule({
1001
1001
  create(context) {
1002
1002
  const bambooFunctionCache = /* @__PURE__ */ new WeakMap();
1003
1003
  const jsxPropertyCache = /* @__PURE__ */ new WeakMap();
@@ -1052,12 +1052,12 @@ const rule$13 = createRule({
1052
1052
  schema: [],
1053
1053
  type: "problem"
1054
1054
  },
1055
- name: RULE_NAME$13
1055
+ name: RULE_NAME$14
1056
1056
  });
1057
1057
  //#endregion
1058
1058
  //#region src/rules/no-invalid-token-paths.ts
1059
- const RULE_NAME$12 = "no-invalid-token-paths";
1060
- const rule$12 = createRule({
1059
+ const RULE_NAME$13 = "no-invalid-token-paths";
1060
+ const rule$13 = createRule({
1061
1061
  create(context) {
1062
1062
  const invalidTokensCache = /* @__PURE__ */ new Map();
1063
1063
  const sendReport = (node, value) => {
@@ -1157,12 +1157,12 @@ const rule$12 = createRule({
1157
1157
  schema: [],
1158
1158
  type: "problem"
1159
1159
  },
1160
- name: RULE_NAME$12
1160
+ name: RULE_NAME$13
1161
1161
  });
1162
1162
  //#endregion
1163
1163
  //#region src/rules/no-margin-properties.ts
1164
- const RULE_NAME$11 = "no-margin-properties";
1165
- const rule$11 = createRule({
1164
+ const RULE_NAME$12 = "no-margin-properties";
1165
+ const rule$12 = createRule({
1166
1166
  create(context) {
1167
1167
  const whitelist = context.options[0]?.whitelist ?? [];
1168
1168
  const longhandCache = /* @__PURE__ */ new Map();
@@ -1238,7 +1238,7 @@ const rule$11 = createRule({
1238
1238
  }],
1239
1239
  type: "suggestion"
1240
1240
  },
1241
- name: RULE_NAME$11
1241
+ name: RULE_NAME$12
1242
1242
  });
1243
1243
  //#endregion
1244
1244
  //#region src/utils/physical-properties.ts
@@ -1282,7 +1282,7 @@ const physicalPropertyValues = { textAlign: {
1282
1282
  } };
1283
1283
  //#endregion
1284
1284
  //#region src/rules/no-physical-properties.ts
1285
- const RULE_NAME$10 = "no-physical-properties";
1285
+ const RULE_NAME$11 = "no-physical-properties";
1286
1286
  const MESSAGES = {
1287
1287
  physical: "Use logical property instead of {{physical}}. Prefer `{{logical}}`.",
1288
1288
  physicalValue: "Use logical value instead of {{physical}}. Prefer `{{logical}}`.",
@@ -1364,7 +1364,7 @@ const createValueReport = (valueNode, valueText, logical, context) => {
1364
1364
  }]
1365
1365
  });
1366
1366
  };
1367
- const rule$10 = createRule({
1367
+ const rule$11 = createRule({
1368
1368
  create(context) {
1369
1369
  const whitelist = context.options[0]?.whitelist ?? [];
1370
1370
  const cache = new PropertyCache();
@@ -1420,12 +1420,12 @@ const rule$10 = createRule({
1420
1420
  }],
1421
1421
  type: "suggestion"
1422
1422
  },
1423
- name: RULE_NAME$10
1423
+ name: RULE_NAME$11
1424
1424
  });
1425
1425
  //#endregion
1426
1426
  //#region src/rules/no-property-renaming.ts
1427
- const RULE_NAME$9 = "no-property-renaming";
1428
- const rule$9 = createRule({
1427
+ const RULE_NAME$10 = "no-property-renaming";
1428
+ const rule$10 = createRule({
1429
1429
  create(context) {
1430
1430
  const bambooPropertyCache = /* @__PURE__ */ new WeakMap();
1431
1431
  const bambooAttributeCache = /* @__PURE__ */ new WeakMap();
@@ -1489,6 +1489,174 @@ const rule$9 = createRule({
1489
1489
  schema: [],
1490
1490
  type: "problem"
1491
1491
  },
1492
+ name: RULE_NAME$10
1493
+ });
1494
+ //#endregion
1495
+ //#region src/rules/no-redundant-value.ts
1496
+ const RULE_NAME$9 = "no-redundant-value";
1497
+ /**
1498
+ * Properties whose value is 1-4 edges in the order top, right, bottom, left, and which therefore
1499
+ * collapse by the same rule: four equal edges are one value, a matching pair is two, a matching
1500
+ * left and right is three.
1501
+ *
1502
+ * An allowlist rather than a shape test, because "several lengths separated by spaces" is not
1503
+ * enough to know the collapse is sound. `background-position: 0 0` is left-top and `0` is
1504
+ * left-centre — same shape, different meaning. Every entry here is a box-edge property where the
1505
+ * CSS specification defines the omitted values as copies of the ones given.
1506
+ *
1507
+ * Bamboo's own shorthands are listed beside the css names they map to, since a project may write
1508
+ * either.
1509
+ */
1510
+ const EDGE_PROPERTIES = new Set([
1511
+ "borderColor",
1512
+ "borderStyle",
1513
+ "borderWidth",
1514
+ "inset",
1515
+ "m",
1516
+ "margin",
1517
+ "p",
1518
+ "padding",
1519
+ "scrollMargin",
1520
+ "scrollPadding"
1521
+ ]);
1522
+ /**
1523
+ * Properties taking one or two values, where a repeated pair collapses to a single value.
1524
+ *
1525
+ * `gap: 8px 8px` is row then column, and one value sets both — the two-value case of the same
1526
+ * idea, kept separate because the four-edge collapse does not apply.
1527
+ */
1528
+ const PAIR_PROPERTIES = new Set([
1529
+ "gap",
1530
+ "gridGap",
1531
+ "overflow",
1532
+ "overscrollBehavior"
1533
+ ]);
1534
+ /** Splits on whitespace, but never inside `var(...)`, `calc(...)` or any other function. */
1535
+ const parts = (value) => {
1536
+ const out = [];
1537
+ let depth = 0;
1538
+ let current = "";
1539
+ for (const char of value) {
1540
+ if (char === "(") depth++;
1541
+ if (char === ")") depth--;
1542
+ if (depth === 0 && /\s/.test(char)) {
1543
+ if (current) out.push(current);
1544
+ current = "";
1545
+ continue;
1546
+ }
1547
+ current += char;
1548
+ }
1549
+ if (current) out.push(current);
1550
+ return depth === 0 ? out : void 0;
1551
+ };
1552
+ /**
1553
+ * Zero with a unit, written as plain zero.
1554
+ *
1555
+ * Every property this rule touches takes lengths, and a zero length is the same zero whatever
1556
+ * unit it carries — including `0%`, which resolves against the containing block to the same
1557
+ * place. Worth doing before the collapse rather than instead of it: `16px 0` and `16px 0px` are
1558
+ * one atom apart on their own, and normalizing first is what lets `0px 16px 0px 16px` reach
1559
+ * `0 16px`.
1560
+ */
1561
+ const ZERO = /^0(?:px|rem|em|%|vh|vw|vmin|vmax|pt|pc|in|cm|mm|ex|ch)$/i;
1562
+ /** The shortest spelling of an edge list that sets the same four edges. */
1563
+ const collapseEdges = (list) => {
1564
+ const [top, right = top, bottom = top, left = right] = list;
1565
+ if (top === bottom && right === left) return top === right ? [top] : [top, right];
1566
+ if (right === left) return [
1567
+ top,
1568
+ right,
1569
+ bottom
1570
+ ];
1571
+ return [
1572
+ top,
1573
+ right,
1574
+ bottom,
1575
+ left
1576
+ ];
1577
+ };
1578
+ const rule$9 = createRule({
1579
+ create(context) {
1580
+ const whitelist = context.options[0]?.whitelist ?? [];
1581
+ const shortest = (property, value) => {
1582
+ const isEdge = EDGE_PROPERTIES.has(property);
1583
+ if (!isEdge && !PAIR_PROPERTIES.has(property)) return;
1584
+ const raw = parts(value);
1585
+ if (!raw) return;
1586
+ const list = raw.map((part) => ZERO.test(part) ? "0" : part);
1587
+ if (isEdge) {
1588
+ if (list.length > 4) return;
1589
+ const shorter = (list.length > 1 ? collapseEdges(list) : list).join(" ");
1590
+ return shorter === value ? void 0 : shorter;
1591
+ }
1592
+ const shorter = list.length === 2 && list[0] === list[1] ? list[0] : list.join(" ");
1593
+ return shorter === value ? void 0 : shorter;
1594
+ };
1595
+ const check = (node, property, value) => {
1596
+ if (whitelist.includes(property)) return;
1597
+ const replacement = shortest(property, value);
1598
+ if (!replacement || replacement === value) return;
1599
+ const data = {
1600
+ property,
1601
+ replacement,
1602
+ value
1603
+ };
1604
+ context.report({
1605
+ data,
1606
+ fix: (fixer) => {
1607
+ const text = context.sourceCode.getText(node);
1608
+ const quote = text.at(0) === "`" || text.at(0) === "\"" || text.at(0) === "'" ? text.at(0) : "'";
1609
+ return fixer.replaceText(node, `${quote}${replacement}${quote}`);
1610
+ },
1611
+ messageId: "redundant",
1612
+ node
1613
+ });
1614
+ };
1615
+ return {
1616
+ JSXAttribute(node) {
1617
+ if (!isJSXIdentifier(node.name)) return;
1618
+ if (!isBambooProp(node, context) || !node.value) return;
1619
+ const property = node.name.name;
1620
+ const valueNode = node.value;
1621
+ if (isLiteral(valueNode)) {
1622
+ check(valueNode, property, valueNode.value?.toString() ?? "");
1623
+ return;
1624
+ }
1625
+ if (!isJSXExpressionContainer(valueNode)) return;
1626
+ const expression = valueNode.expression;
1627
+ if (isLiteral(expression)) check(expression, property, expression.value?.toString() ?? "");
1628
+ else if (isTemplateLiteral(expression) && expression.expressions.length === 0) check(expression, property, expression.quasis[0].value.raw);
1629
+ },
1630
+ Property(node) {
1631
+ if (!isIdentifier(node.key)) return;
1632
+ if (!isBambooAttribute(node, context)) return;
1633
+ if (isRecipeVariant(node, context)) return;
1634
+ const property = node.key.name;
1635
+ const valueNode = node.value;
1636
+ if (isLiteral(valueNode)) check(valueNode, property, valueNode.value?.toString() ?? "");
1637
+ else if (isTemplateLiteral(valueNode) && valueNode.expressions.length === 0) check(valueNode, property, valueNode.quasis[0].value.raw);
1638
+ }
1639
+ };
1640
+ },
1641
+ defaultOptions: [{ whitelist: [] }],
1642
+ meta: {
1643
+ docs: { description: "Report an edge or pair value written longer than it needs to be, where a shorter spelling sets exactly the same properties." },
1644
+ fixable: "code",
1645
+ messages: { redundant: "`{{value}}` sets the same edges as `{{replacement}}`." },
1646
+ schema: [{
1647
+ additionalProperties: false,
1648
+ properties: { whitelist: {
1649
+ items: {
1650
+ minLength: 0,
1651
+ type: "string"
1652
+ },
1653
+ type: "array",
1654
+ uniqueItems: true
1655
+ } },
1656
+ type: "object"
1657
+ }],
1658
+ type: "suggestion"
1659
+ },
1492
1660
  name: RULE_NAME$9
1493
1661
  });
1494
1662
  //#endregion
@@ -2625,6 +2793,7 @@ const rule = createRule({
2625
2793
  //#endregion
2626
2794
  //#region src/rules/index.ts
2627
2795
  const rules = {
2796
+ [RULE_NAME$23]: rule$23,
2628
2797
  [RULE_NAME$22]: rule$22,
2629
2798
  [RULE_NAME$21]: rule$21,
2630
2799
  [RULE_NAME$20]: rule$20,
@@ -2652,9 +2821,9 @@ const rules = {
2652
2821
  //#endregion
2653
2822
  //#region src/configs/all.ts
2654
2823
  const errorRules = [
2824
+ RULE_NAME$23,
2655
2825
  RULE_NAME$22,
2656
- RULE_NAME$21,
2657
- RULE_NAME$12
2826
+ RULE_NAME$13
2658
2827
  ];
2659
2828
  //#endregion
2660
2829
  //#region src/index.ts
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import "@typescript-eslint/utils/ts-eslint";
8
8
  import { getArbitraryValue, parseFallbackValue } from "@bamboocss/shared";
9
9
  //#region package.json
10
10
  var name = "@bamboocss/eslint-plugin";
11
- var version = "1.44.1";
11
+ var version = "1.45.0";
12
12
  //#endregion
13
13
  //#region src/utils/index.ts
14
14
  const createRule = ESLintUtils.RuleCreator((name) => `https://github.com/gajus/bamboocss/blob/main/packages/eslint-plugin/docs/rules/${name}.md`);
@@ -318,8 +318,8 @@ function isRecipeVariant(node, context) {
318
318
  }
319
319
  //#endregion
320
320
  //#region src/rules/file-not-included.ts
321
- const RULE_NAME$22 = "file-not-included";
322
- const rule$22 = createRule({
321
+ const RULE_NAME$23 = "file-not-included";
322
+ const rule$23 = createRule({
323
323
  create(context) {
324
324
  if (isValidFile(context)) return {};
325
325
  let hasReported = false;
@@ -340,11 +340,11 @@ const rule$22 = createRule({
340
340
  schema: [],
341
341
  type: "problem"
342
342
  },
343
- name: RULE_NAME$22
343
+ name: RULE_NAME$23
344
344
  });
345
345
  //#endregion
346
346
  //#region src/rules/no-config-function-in-source.ts
347
- const RULE_NAME$21 = "no-config-function-in-source";
347
+ const RULE_NAME$22 = "no-config-function-in-source";
348
348
  const CONFIG_FUNCTIONS = new Set([
349
349
  "defineConfig",
350
350
  "defineGlobalStyles",
@@ -359,7 +359,7 @@ const CONFIG_FUNCTIONS = new Set([
359
359
  "defineTokens",
360
360
  "defineUtility"
361
361
  ]);
362
- const rule$21 = createRule({
362
+ const rule$22 = createRule({
363
363
  create(context) {
364
364
  if (!hasPkgImport(context)) return {};
365
365
  if (!isValidFile(context)) return {};
@@ -399,12 +399,12 @@ const rule$21 = createRule({
399
399
  schema: [],
400
400
  type: "problem"
401
401
  },
402
- name: RULE_NAME$21
402
+ name: RULE_NAME$22
403
403
  });
404
404
  //#endregion
405
405
  //#region src/rules/no-debug.ts
406
- const RULE_NAME$20 = "no-debug";
407
- const rule$20 = createRule({
406
+ const RULE_NAME$21 = "no-debug";
407
+ const rule$21 = createRule({
408
408
  create(context) {
409
409
  const processedNodes = /* @__PURE__ */ new WeakSet();
410
410
  const checkObjectForDebug = (object) => {
@@ -473,12 +473,12 @@ const rule$20 = createRule({
473
473
  schema: [],
474
474
  type: "problem"
475
475
  },
476
- name: RULE_NAME$20
476
+ name: RULE_NAME$21
477
477
  });
478
478
  //#endregion
479
479
  //#region src/rules/no-deprecated-tokens.ts
480
- const RULE_NAME$19 = "no-deprecated-tokens";
481
- const rule$19 = createRule({
480
+ const RULE_NAME$20 = "no-deprecated-tokens";
481
+ const rule$20 = createRule({
482
482
  create(context) {
483
483
  const deprecatedTokensCache = /* @__PURE__ */ new Map();
484
484
  const sendReport = (property, node, value) => {
@@ -532,11 +532,11 @@ const rule$19 = createRule({
532
532
  schema: [],
533
533
  type: "problem"
534
534
  },
535
- name: RULE_NAME$19
535
+ name: RULE_NAME$20
536
536
  });
537
537
  //#endregion
538
538
  //#region src/rules/no-descendant-selectors.ts
539
- const RULE_NAME$18 = "no-descendant-selectors";
539
+ const RULE_NAME$19 = "no-descendant-selectors";
540
540
  /**
541
541
  * Split a selector on combinators that are not inside brackets, parentheses or quotes.
542
542
  *
@@ -602,7 +602,7 @@ const selectorOf = (node) => {
602
602
  if (isLiteral(node) && typeof node.value === "string") return node.value;
603
603
  if (isTemplateLiteral(node) && node.expressions.length === 0) return node.quasis[0]?.value.raw;
604
604
  };
605
- const rule$18 = createRule({
605
+ const rule$19 = createRule({
606
606
  create(context) {
607
607
  return { "Property[key.type!=/Identifier/][value.type=\"ObjectExpression\"]"(node) {
608
608
  const selector = selectorOf(node.key);
@@ -626,12 +626,12 @@ const rule$18 = createRule({
626
626
  schema: [],
627
627
  type: "problem"
628
628
  },
629
- name: RULE_NAME$18
629
+ name: RULE_NAME$19
630
630
  });
631
631
  //#endregion
632
632
  //#region src/rules/no-dynamic-styling.ts
633
- const RULE_NAME$17 = "no-dynamic-styling";
634
- const rule$17 = createRule({
633
+ const RULE_NAME$18 = "no-dynamic-styling";
634
+ const rule$18 = createRule({
635
635
  create(context) {
636
636
  function isStaticValue(node) {
637
637
  if (!node) return false;
@@ -702,12 +702,12 @@ const rule$17 = createRule({
702
702
  schema: [],
703
703
  type: "problem"
704
704
  },
705
- name: RULE_NAME$17
705
+ name: RULE_NAME$18
706
706
  });
707
707
  //#endregion
708
708
  //#region src/rules/no-escape-hatch.ts
709
- const RULE_NAME$16 = "no-escape-hatch";
710
- const rule$16 = createRule({
709
+ const RULE_NAME$17 = "no-escape-hatch";
710
+ const rule$17 = createRule({
711
711
  create(context) {
712
712
  const removeBrackets = (range) => {
713
713
  const [start, end] = range;
@@ -780,12 +780,12 @@ const rule$16 = createRule({
780
780
  schema: [],
781
781
  type: "problem"
782
782
  },
783
- name: RULE_NAME$16
783
+ name: RULE_NAME$17
784
784
  });
785
785
  //#endregion
786
786
  //#region src/rules/no-hardcoded-color.ts
787
- const RULE_NAME$15 = "no-hardcoded-color";
788
- const rule$15 = createRule({
787
+ const RULE_NAME$16 = "no-hardcoded-color";
788
+ const rule$16 = createRule({
789
789
  create(context) {
790
790
  const noOpacity = context.options[0]?.noOpacity;
791
791
  const whitelist = context.options[0]?.whitelist ?? [];
@@ -881,14 +881,14 @@ const rule$15 = createRule({
881
881
  }],
882
882
  type: "problem"
883
883
  },
884
- name: RULE_NAME$15
884
+ name: RULE_NAME$16
885
885
  });
886
886
  //#endregion
887
887
  //#region src/rules/no-important.ts
888
888
  const exclamationRegex = /\s*!$/;
889
889
  const importantRegex = /\s*!important\s*$/;
890
- const RULE_NAME$14 = "no-important";
891
- const rule$14 = createRule({
890
+ const RULE_NAME$15 = "no-important";
891
+ const rule$15 = createRule({
892
892
  create(context) {
893
893
  const removeQuotes = (range) => {
894
894
  const [start, end] = range;
@@ -992,12 +992,12 @@ const rule$14 = createRule({
992
992
  schema: [],
993
993
  type: "problem"
994
994
  },
995
- name: RULE_NAME$14
995
+ name: RULE_NAME$15
996
996
  });
997
997
  //#endregion
998
998
  //#region src/rules/no-invalid-nesting.ts
999
- const RULE_NAME$13 = "no-invalid-nesting";
1000
- const rule$13 = createRule({
999
+ const RULE_NAME$14 = "no-invalid-nesting";
1000
+ const rule$14 = createRule({
1001
1001
  create(context) {
1002
1002
  const bambooFunctionCache = /* @__PURE__ */ new WeakMap();
1003
1003
  const jsxPropertyCache = /* @__PURE__ */ new WeakMap();
@@ -1052,12 +1052,12 @@ const rule$13 = createRule({
1052
1052
  schema: [],
1053
1053
  type: "problem"
1054
1054
  },
1055
- name: RULE_NAME$13
1055
+ name: RULE_NAME$14
1056
1056
  });
1057
1057
  //#endregion
1058
1058
  //#region src/rules/no-invalid-token-paths.ts
1059
- const RULE_NAME$12 = "no-invalid-token-paths";
1060
- const rule$12 = createRule({
1059
+ const RULE_NAME$13 = "no-invalid-token-paths";
1060
+ const rule$13 = createRule({
1061
1061
  create(context) {
1062
1062
  const invalidTokensCache = /* @__PURE__ */ new Map();
1063
1063
  const sendReport = (node, value) => {
@@ -1157,12 +1157,12 @@ const rule$12 = createRule({
1157
1157
  schema: [],
1158
1158
  type: "problem"
1159
1159
  },
1160
- name: RULE_NAME$12
1160
+ name: RULE_NAME$13
1161
1161
  });
1162
1162
  //#endregion
1163
1163
  //#region src/rules/no-margin-properties.ts
1164
- const RULE_NAME$11 = "no-margin-properties";
1165
- const rule$11 = createRule({
1164
+ const RULE_NAME$12 = "no-margin-properties";
1165
+ const rule$12 = createRule({
1166
1166
  create(context) {
1167
1167
  const whitelist = context.options[0]?.whitelist ?? [];
1168
1168
  const longhandCache = /* @__PURE__ */ new Map();
@@ -1238,7 +1238,7 @@ const rule$11 = createRule({
1238
1238
  }],
1239
1239
  type: "suggestion"
1240
1240
  },
1241
- name: RULE_NAME$11
1241
+ name: RULE_NAME$12
1242
1242
  });
1243
1243
  //#endregion
1244
1244
  //#region src/utils/physical-properties.ts
@@ -1282,7 +1282,7 @@ const physicalPropertyValues = { textAlign: {
1282
1282
  } };
1283
1283
  //#endregion
1284
1284
  //#region src/rules/no-physical-properties.ts
1285
- const RULE_NAME$10 = "no-physical-properties";
1285
+ const RULE_NAME$11 = "no-physical-properties";
1286
1286
  const MESSAGES = {
1287
1287
  physical: "Use logical property instead of {{physical}}. Prefer `{{logical}}`.",
1288
1288
  physicalValue: "Use logical value instead of {{physical}}. Prefer `{{logical}}`.",
@@ -1364,7 +1364,7 @@ const createValueReport = (valueNode, valueText, logical, context) => {
1364
1364
  }]
1365
1365
  });
1366
1366
  };
1367
- const rule$10 = createRule({
1367
+ const rule$11 = createRule({
1368
1368
  create(context) {
1369
1369
  const whitelist = context.options[0]?.whitelist ?? [];
1370
1370
  const cache = new PropertyCache();
@@ -1420,12 +1420,12 @@ const rule$10 = createRule({
1420
1420
  }],
1421
1421
  type: "suggestion"
1422
1422
  },
1423
- name: RULE_NAME$10
1423
+ name: RULE_NAME$11
1424
1424
  });
1425
1425
  //#endregion
1426
1426
  //#region src/rules/no-property-renaming.ts
1427
- const RULE_NAME$9 = "no-property-renaming";
1428
- const rule$9 = createRule({
1427
+ const RULE_NAME$10 = "no-property-renaming";
1428
+ const rule$10 = createRule({
1429
1429
  create(context) {
1430
1430
  const bambooPropertyCache = /* @__PURE__ */ new WeakMap();
1431
1431
  const bambooAttributeCache = /* @__PURE__ */ new WeakMap();
@@ -1489,6 +1489,174 @@ const rule$9 = createRule({
1489
1489
  schema: [],
1490
1490
  type: "problem"
1491
1491
  },
1492
+ name: RULE_NAME$10
1493
+ });
1494
+ //#endregion
1495
+ //#region src/rules/no-redundant-value.ts
1496
+ const RULE_NAME$9 = "no-redundant-value";
1497
+ /**
1498
+ * Properties whose value is 1-4 edges in the order top, right, bottom, left, and which therefore
1499
+ * collapse by the same rule: four equal edges are one value, a matching pair is two, a matching
1500
+ * left and right is three.
1501
+ *
1502
+ * An allowlist rather than a shape test, because "several lengths separated by spaces" is not
1503
+ * enough to know the collapse is sound. `background-position: 0 0` is left-top and `0` is
1504
+ * left-centre — same shape, different meaning. Every entry here is a box-edge property where the
1505
+ * CSS specification defines the omitted values as copies of the ones given.
1506
+ *
1507
+ * Bamboo's own shorthands are listed beside the css names they map to, since a project may write
1508
+ * either.
1509
+ */
1510
+ const EDGE_PROPERTIES = new Set([
1511
+ "borderColor",
1512
+ "borderStyle",
1513
+ "borderWidth",
1514
+ "inset",
1515
+ "m",
1516
+ "margin",
1517
+ "p",
1518
+ "padding",
1519
+ "scrollMargin",
1520
+ "scrollPadding"
1521
+ ]);
1522
+ /**
1523
+ * Properties taking one or two values, where a repeated pair collapses to a single value.
1524
+ *
1525
+ * `gap: 8px 8px` is row then column, and one value sets both — the two-value case of the same
1526
+ * idea, kept separate because the four-edge collapse does not apply.
1527
+ */
1528
+ const PAIR_PROPERTIES = new Set([
1529
+ "gap",
1530
+ "gridGap",
1531
+ "overflow",
1532
+ "overscrollBehavior"
1533
+ ]);
1534
+ /** Splits on whitespace, but never inside `var(...)`, `calc(...)` or any other function. */
1535
+ const parts = (value) => {
1536
+ const out = [];
1537
+ let depth = 0;
1538
+ let current = "";
1539
+ for (const char of value) {
1540
+ if (char === "(") depth++;
1541
+ if (char === ")") depth--;
1542
+ if (depth === 0 && /\s/.test(char)) {
1543
+ if (current) out.push(current);
1544
+ current = "";
1545
+ continue;
1546
+ }
1547
+ current += char;
1548
+ }
1549
+ if (current) out.push(current);
1550
+ return depth === 0 ? out : void 0;
1551
+ };
1552
+ /**
1553
+ * Zero with a unit, written as plain zero.
1554
+ *
1555
+ * Every property this rule touches takes lengths, and a zero length is the same zero whatever
1556
+ * unit it carries — including `0%`, which resolves against the containing block to the same
1557
+ * place. Worth doing before the collapse rather than instead of it: `16px 0` and `16px 0px` are
1558
+ * one atom apart on their own, and normalizing first is what lets `0px 16px 0px 16px` reach
1559
+ * `0 16px`.
1560
+ */
1561
+ const ZERO = /^0(?:px|rem|em|%|vh|vw|vmin|vmax|pt|pc|in|cm|mm|ex|ch)$/i;
1562
+ /** The shortest spelling of an edge list that sets the same four edges. */
1563
+ const collapseEdges = (list) => {
1564
+ const [top, right = top, bottom = top, left = right] = list;
1565
+ if (top === bottom && right === left) return top === right ? [top] : [top, right];
1566
+ if (right === left) return [
1567
+ top,
1568
+ right,
1569
+ bottom
1570
+ ];
1571
+ return [
1572
+ top,
1573
+ right,
1574
+ bottom,
1575
+ left
1576
+ ];
1577
+ };
1578
+ const rule$9 = createRule({
1579
+ create(context) {
1580
+ const whitelist = context.options[0]?.whitelist ?? [];
1581
+ const shortest = (property, value) => {
1582
+ const isEdge = EDGE_PROPERTIES.has(property);
1583
+ if (!isEdge && !PAIR_PROPERTIES.has(property)) return;
1584
+ const raw = parts(value);
1585
+ if (!raw) return;
1586
+ const list = raw.map((part) => ZERO.test(part) ? "0" : part);
1587
+ if (isEdge) {
1588
+ if (list.length > 4) return;
1589
+ const shorter = (list.length > 1 ? collapseEdges(list) : list).join(" ");
1590
+ return shorter === value ? void 0 : shorter;
1591
+ }
1592
+ const shorter = list.length === 2 && list[0] === list[1] ? list[0] : list.join(" ");
1593
+ return shorter === value ? void 0 : shorter;
1594
+ };
1595
+ const check = (node, property, value) => {
1596
+ if (whitelist.includes(property)) return;
1597
+ const replacement = shortest(property, value);
1598
+ if (!replacement || replacement === value) return;
1599
+ const data = {
1600
+ property,
1601
+ replacement,
1602
+ value
1603
+ };
1604
+ context.report({
1605
+ data,
1606
+ fix: (fixer) => {
1607
+ const text = context.sourceCode.getText(node);
1608
+ const quote = text.at(0) === "`" || text.at(0) === "\"" || text.at(0) === "'" ? text.at(0) : "'";
1609
+ return fixer.replaceText(node, `${quote}${replacement}${quote}`);
1610
+ },
1611
+ messageId: "redundant",
1612
+ node
1613
+ });
1614
+ };
1615
+ return {
1616
+ JSXAttribute(node) {
1617
+ if (!isJSXIdentifier(node.name)) return;
1618
+ if (!isBambooProp(node, context) || !node.value) return;
1619
+ const property = node.name.name;
1620
+ const valueNode = node.value;
1621
+ if (isLiteral(valueNode)) {
1622
+ check(valueNode, property, valueNode.value?.toString() ?? "");
1623
+ return;
1624
+ }
1625
+ if (!isJSXExpressionContainer(valueNode)) return;
1626
+ const expression = valueNode.expression;
1627
+ if (isLiteral(expression)) check(expression, property, expression.value?.toString() ?? "");
1628
+ else if (isTemplateLiteral(expression) && expression.expressions.length === 0) check(expression, property, expression.quasis[0].value.raw);
1629
+ },
1630
+ Property(node) {
1631
+ if (!isIdentifier(node.key)) return;
1632
+ if (!isBambooAttribute(node, context)) return;
1633
+ if (isRecipeVariant(node, context)) return;
1634
+ const property = node.key.name;
1635
+ const valueNode = node.value;
1636
+ if (isLiteral(valueNode)) check(valueNode, property, valueNode.value?.toString() ?? "");
1637
+ else if (isTemplateLiteral(valueNode) && valueNode.expressions.length === 0) check(valueNode, property, valueNode.quasis[0].value.raw);
1638
+ }
1639
+ };
1640
+ },
1641
+ defaultOptions: [{ whitelist: [] }],
1642
+ meta: {
1643
+ docs: { description: "Report an edge or pair value written longer than it needs to be, where a shorter spelling sets exactly the same properties." },
1644
+ fixable: "code",
1645
+ messages: { redundant: "`{{value}}` sets the same edges as `{{replacement}}`." },
1646
+ schema: [{
1647
+ additionalProperties: false,
1648
+ properties: { whitelist: {
1649
+ items: {
1650
+ minLength: 0,
1651
+ type: "string"
1652
+ },
1653
+ type: "array",
1654
+ uniqueItems: true
1655
+ } },
1656
+ type: "object"
1657
+ }],
1658
+ type: "suggestion"
1659
+ },
1492
1660
  name: RULE_NAME$9
1493
1661
  });
1494
1662
  //#endregion
@@ -2625,6 +2793,7 @@ const rule = createRule({
2625
2793
  //#endregion
2626
2794
  //#region src/rules/index.ts
2627
2795
  const rules = {
2796
+ [RULE_NAME$23]: rule$23,
2628
2797
  [RULE_NAME$22]: rule$22,
2629
2798
  [RULE_NAME$21]: rule$21,
2630
2799
  [RULE_NAME$20]: rule$20,
@@ -2652,9 +2821,9 @@ const rules = {
2652
2821
  //#endregion
2653
2822
  //#region src/configs/all.ts
2654
2823
  const errorRules = [
2824
+ RULE_NAME$23,
2655
2825
  RULE_NAME$22,
2656
- RULE_NAME$21,
2657
- RULE_NAME$12
2826
+ RULE_NAME$13
2658
2827
  ];
2659
2828
  //#endregion
2660
2829
  //#region src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/eslint-plugin",
3
- "version": "1.44.1",
3
+ "version": "1.45.0",
4
4
  "description": "ESLint plugin for Bamboo CSS",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "@typescript-eslint/utils": "^8.21.0",
37
37
  "micromatch": "^4.0.8",
38
38
  "synckit": "^0.9.0",
39
- "@bamboocss/config": "1.44.1",
40
- "@bamboocss/generator": "1.44.1",
41
- "@bamboocss/shared": "1.44.1"
39
+ "@bamboocss/config": "1.45.0",
40
+ "@bamboocss/generator": "1.45.0",
41
+ "@bamboocss/shared": "1.45.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@types/micromatch": "^4.0.10",
@@ -46,10 +46,10 @@
46
46
  "eslint": "^9.35.0",
47
47
  "multiline-ts": "^4.0.1",
48
48
  "typescript": "^5.7.2",
49
- "@bamboocss/dev": "1.44.1",
50
- "@bamboocss/preset-bamboo": "1.44.1",
51
- "@bamboocss/preset-base": "1.44.1",
52
- "@bamboocss/types": "1.44.1"
49
+ "@bamboocss/dev": "1.45.0",
50
+ "@bamboocss/preset-bamboo": "1.45.0",
51
+ "@bamboocss/preset-base": "1.45.0",
52
+ "@bamboocss/types": "1.45.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "eslint": "*"