@arcgis/eslint-config 6.0.0-next.13 → 6.0.0-next.15

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.
@@ -0,0 +1,31 @@
1
+ import { AST_NODE_TYPES, ASTUtils, TSESLint } from "@typescript-eslint/utils";
2
+ function isImportedSymbol(node, sourceCode, moduleName, names) {
3
+ const identifier = node.type === AST_NODE_TYPES.MemberExpression ? node.object : node;
4
+ if (identifier.type !== AST_NODE_TYPES.Identifier) {
5
+ return false;
6
+ }
7
+ const variable = ASTUtils.findVariable(sourceCode.getScope(identifier), identifier.name);
8
+ const definition = variable?.defs[0];
9
+ if (definition?.type !== TSESLint.Scope.DefinitionType.ImportBinding || definition.parent.type !== AST_NODE_TYPES.ImportDeclaration || definition.parent.source.value !== moduleName || definition.parent.importKind === "type") {
10
+ return false;
11
+ }
12
+ const specifier = definition.node;
13
+ if (node.type === AST_NODE_TYPES.MemberExpression) {
14
+ return specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier && names.includes(propertyName(node) ?? "");
15
+ }
16
+ if (specifier.type !== AST_NODE_TYPES.ImportSpecifier || specifier.importKind === "type") {
17
+ return false;
18
+ }
19
+ const imported = specifier.imported;
20
+ return names.includes(imported.type === AST_NODE_TYPES.Identifier ? imported.name : imported.value);
21
+ }
22
+ function propertyName(node) {
23
+ if (!node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
24
+ return node.property.name;
25
+ }
26
+ return node.property.type === AST_NODE_TYPES.Literal && typeof node.property.value === "string" ? node.property.value : void 0;
27
+ }
28
+ export {
29
+ isImportedSymbol as i,
30
+ propertyName as p
31
+ };
@@ -814,6 +814,14 @@ const eslintConfigCore = [
814
814
  message: "Avoid adding has flags manually. Instead use the suite/test-level options to set has flags."
815
815
  }
816
816
  ],
817
+ "webgis/no-unsafe-fake-timers": [
818
+ "error",
819
+ {
820
+ fakeTimerHelpers: [
821
+ { module: "test-support/schedulingUtils", names: ["useFakeTimers", "useFastForwardTimers"] }
822
+ ]
823
+ }
824
+ ],
817
825
  // Require awaiting assert.screenshot() in tests
818
826
  "webgis/require-await-for-async-screenshot-assert": "error"
819
827
  }
@@ -822,7 +830,7 @@ const eslintConfigCore = [
822
830
  files: ["packages/core-packages/core/tests/**/*.{ts,tsx}"],
823
831
  ignores: ["packages/core-packages/core/tests/support/schedulingUtils.ts"],
824
832
  rules: {
825
- "core/no-unsafe-fake-timers": "error"
833
+ "core/require-managed-timers": "error"
826
834
  }
827
835
  }
828
836
  ];
@@ -5,6 +5,7 @@ import path, { relative, win32, posix, normalize } from "path";
5
5
  import { dirname, join } from "node:path";
6
6
  import { existsSync } from "node:fs";
7
7
  import { execSync } from "child_process";
8
+ import { p as propertyName, i as isImportedSymbol } from "../../chunks/imports.js";
8
9
  const plugin = makeEslintPlugin(
9
10
  "core",
10
11
  (rule) => `https://devtopia.esri.com/WebGIS/webgis-sdk/tree/main/packages/support-packages/eslint-config/src/plugins/core/rules/${rule}.ts`
@@ -483,103 +484,6 @@ function getDeclaredClass(filename) {
483
484
  const relativePath = relative(root, filename).replaceAll(win32.sep, ".").replaceAll(posix.sep, ".").replace(/^src\./u, "esri.").replace(/\.[^/.]+$/u, "");
484
485
  return relativePath;
485
486
  }
486
- const timerHelpers = /* @__PURE__ */ new Set(["useFakeTimers", "useFastForwardTimers"]);
487
- const timerGlobals = /* @__PURE__ */ new Set([
488
- "setTimeout",
489
- "clearTimeout",
490
- "setInterval",
491
- "clearInterval",
492
- "requestAnimationFrame",
493
- "cancelAnimationFrame",
494
- "requestIdleCallback",
495
- "cancelIdleCallback",
496
- "queueMicrotask",
497
- "Date"
498
- ]);
499
- plugin.createRule({
500
- name: "no-unsafe-fake-timers",
501
- meta: {
502
- docs: {
503
- description: "Use core's managed timer helpers and do not mix fake timers with global timer spies.",
504
- defaultLevel: "error",
505
- isTypeChecked: false
506
- },
507
- messages: {
508
- managedTimers: "Use useFakeTimers or useFastForwardTimers from test-support/schedulingUtils. They restore timers on disposal and test completion.",
509
- timerSpy: "Do not mix fake timers with global timer spies in the same file. Vitest can reinstall fake timers when restoring disposed spies. Use vi.getTimerCount() or observe timer callbacks instead."
510
- },
511
- schema: [],
512
- type: "problem"
513
- },
514
- defaultOptions: [],
515
- create(context) {
516
- const vitestNames = /* @__PURE__ */ new Set();
517
- const vitestNamespaces = /* @__PURE__ */ new Set();
518
- const helperNames = /* @__PURE__ */ new Set();
519
- const helperNamespaces = /* @__PURE__ */ new Set();
520
- const timerSpies = [];
521
- let usesFakeTimers = false;
522
- function isVitest(node) {
523
- return node.type === AST_NODE_TYPES.Identifier && vitestNames.has(node.name) || node.type === AST_NODE_TYPES.MemberExpression && node.object.type === AST_NODE_TYPES.Identifier && vitestNamespaces.has(node.object.name) && ["vi", "vitest"].includes(propertyName(node) ?? "");
524
- }
525
- return {
526
- "ImportDeclaration"(node) {
527
- const isVitestImport = node.source.value === "vitest";
528
- if (!isVitestImport && node.source.value !== "test-support/schedulingUtils") {
529
- return;
530
- }
531
- for (const specifier of node.specifiers) {
532
- if (specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier) {
533
- (isVitestImport ? vitestNamespaces : helperNamespaces).add(specifier.local.name);
534
- } else if (specifier.type === AST_NODE_TYPES.ImportSpecifier) {
535
- const imported = specifier.imported.type === AST_NODE_TYPES.Identifier ? specifier.imported.name : specifier.imported.value;
536
- if (isVitestImport ? imported === "vi" || imported === "vitest" : timerHelpers.has(imported)) {
537
- (isVitestImport ? vitestNames : helperNames).add(specifier.local.name);
538
- }
539
- }
540
- }
541
- },
542
- "CallExpression"(node) {
543
- const callee = node.callee;
544
- if (callee.type === AST_NODE_TYPES.Identifier && helperNames.has(callee.name)) {
545
- usesFakeTimers = true;
546
- }
547
- if (callee.type !== AST_NODE_TYPES.MemberExpression) {
548
- return;
549
- }
550
- const method = propertyName(callee);
551
- if (callee.object.type === AST_NODE_TYPES.Identifier && helperNamespaces.has(callee.object.name) && method != null && timerHelpers.has(method)) {
552
- usesFakeTimers = true;
553
- }
554
- if (!isVitest(callee.object)) {
555
- return;
556
- }
557
- if (method === "useFakeTimers" || method === "useRealTimers") {
558
- usesFakeTimers ||= method === "useFakeTimers";
559
- context.report({ node, messageId: "managedTimers" });
560
- } else if (method === "spyOn") {
561
- const [target, property] = node.arguments;
562
- if (target?.type === AST_NODE_TYPES.Identifier && ["globalThis", "window", "global", "self"].includes(target.name) && property?.type === AST_NODE_TYPES.Literal && typeof property.value === "string" && timerGlobals.has(property.value)) {
563
- timerSpies.push(node);
564
- }
565
- }
566
- },
567
- "Program:exit"() {
568
- if (usesFakeTimers) {
569
- for (const node of timerSpies) {
570
- context.report({ node, messageId: "timerSpy" });
571
- }
572
- }
573
- }
574
- };
575
- }
576
- });
577
- function propertyName(node) {
578
- if (!node.computed && node.property.type === AST_NODE_TYPES.Identifier) {
579
- return node.property.name;
580
- }
581
- return node.property.type === AST_NODE_TYPES.Literal && typeof node.property.value === "string" ? node.property.value : void 0;
582
- }
583
487
  const tsRe = /\.tsx?$/u;
584
488
  let gitIndexFiles;
585
489
  function getGitIndexFiles(cwd) {
@@ -773,6 +677,32 @@ plugin.createRule({
773
677
  };
774
678
  }
775
679
  });
680
+ plugin.createRule({
681
+ name: "require-managed-timers",
682
+ meta: {
683
+ docs: {
684
+ description: "Use core's managed timer helpers instead of direct Vitest timer setup and restoration.",
685
+ defaultLevel: "error",
686
+ isTypeChecked: false
687
+ },
688
+ messages: {
689
+ managedTimers: "Use useFakeTimers or useFastForwardTimers from test-support/schedulingUtils. They restore timers on disposal and test completion."
690
+ },
691
+ schema: [],
692
+ type: "problem"
693
+ },
694
+ defaultOptions: [],
695
+ create(context) {
696
+ return {
697
+ CallExpression(node) {
698
+ const callee = node.callee;
699
+ if (callee.type === AST_NODE_TYPES.MemberExpression && ["useFakeTimers", "useRealTimers"].includes(propertyName(callee) ?? "") && isImportedSymbol(callee.object, context.sourceCode, "vitest", ["vi", "vitest"])) {
700
+ context.report({ node, messageId: "managedTimers" });
701
+ }
702
+ }
703
+ };
704
+ }
705
+ });
776
706
  plugin.createRule({
777
707
  name: "spec-suite-filename",
778
708
  meta: {
@@ -1,9 +1,10 @@
1
1
  import { m as makeEslintPlugin } from "../../chunks/makePlugin.js";
2
- import { TSESTree, AST_TOKEN_TYPES, AST_NODE_TYPES, TSESLint } from "@typescript-eslint/utils";
2
+ import { TSESTree, AST_TOKEN_TYPES, AST_NODE_TYPES, TSESLint, ASTUtils } from "@typescript-eslint/utils";
3
3
  import path from "node:path";
4
4
  import { resolve } from "path/posix";
5
5
  import globals from "globals";
6
6
  import { uncapitalize, kebabToPascal } from "@arcgis/toolkit/string";
7
+ import { i as isImportedSymbol, p as propertyName } from "../../chunks/imports.js";
7
8
  import { e as extractDeclareElementsInterface, k as getComponentDeclaration } from "../../chunks/estree.js";
8
9
  const plugin = makeEslintPlugin(
9
10
  "webgis",
@@ -907,6 +908,86 @@ plugin.createRule({
907
908
  }
908
909
  });
909
910
  const reTouchingJsDoc = /\*\/\s+\/\*\*/gu;
911
+ const timerGlobals = /* @__PURE__ */ new Set([
912
+ "setTimeout",
913
+ "clearTimeout",
914
+ "setInterval",
915
+ "clearInterval",
916
+ "setImmediate",
917
+ "clearImmediate",
918
+ "requestAnimationFrame",
919
+ "cancelAnimationFrame",
920
+ "requestIdleCallback",
921
+ "cancelIdleCallback",
922
+ "queueMicrotask",
923
+ "Date"
924
+ ]);
925
+ plugin.createRule({
926
+ name: "no-unsafe-fake-timers",
927
+ meta: {
928
+ docs: {
929
+ description: "Do not mix fake timers with global timer spies, even across tests in the same file.",
930
+ defaultLevel: "error",
931
+ isTypeChecked: false
932
+ },
933
+ messages: {
934
+ timerSpy: "Do not mix fake timers with global timer spies in the same file. Even disposed spies can corrupt timers during later mock cleanup. Use vi.getTimerCount() or observe timer callbacks instead."
935
+ },
936
+ schema: [
937
+ {
938
+ type: "object",
939
+ properties: {
940
+ fakeTimerHelpers: {
941
+ type: "array",
942
+ items: {
943
+ type: "object",
944
+ properties: {
945
+ module: { type: "string" },
946
+ names: { type: "array", items: { type: "string" }, minItems: 1, uniqueItems: true }
947
+ },
948
+ required: ["module", "names"],
949
+ additionalProperties: false
950
+ }
951
+ }
952
+ },
953
+ additionalProperties: false
954
+ }
955
+ ],
956
+ type: "problem"
957
+ },
958
+ defaultOptions: [{}],
959
+ create(context, [{ fakeTimerHelpers = [] }]) {
960
+ const timerSpies = [];
961
+ let usesFakeTimers = false;
962
+ return {
963
+ "CallExpression"(node) {
964
+ const callee = node.callee;
965
+ usesFakeTimers ||= fakeTimerHelpers.some(
966
+ ({ module, names }) => isImportedSymbol(callee, context.sourceCode, module, names)
967
+ );
968
+ if (callee.type !== AST_NODE_TYPES.MemberExpression || !isImportedSymbol(callee.object, context.sourceCode, "vitest", ["vi", "vitest"])) {
969
+ return;
970
+ }
971
+ const method = propertyName(callee);
972
+ if (method === "useFakeTimers") {
973
+ usesFakeTimers = true;
974
+ } else if (method === "spyOn") {
975
+ const [target, property] = node.arguments;
976
+ if (target?.type === AST_NODE_TYPES.Identifier && ["globalThis", "window", "global", "self"].includes(target.name) && property?.type === AST_NODE_TYPES.Literal && typeof property.value === "string" && timerGlobals.has(property.value) && !ASTUtils.findVariable(context.sourceCode.getScope(target), target.name)?.defs.length) {
977
+ timerSpies.push(node);
978
+ }
979
+ }
980
+ },
981
+ "Program:exit"() {
982
+ if (usesFakeTimers) {
983
+ for (const node of timerSpies) {
984
+ context.report({ node, messageId: "timerSpy" });
985
+ }
986
+ }
987
+ }
988
+ };
989
+ }
990
+ });
910
991
  const description$1 = `Do not use links like [](#something) as they are not portable across .d.ts, and are not validated at build time. Use {@link } syntax, or absolute links. See https://webgis.esri.com/references/api-extractor/tags-reference#link)`;
911
992
  plugin.createRule({
912
993
  name: "no-unsafe-hash-links",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arcgis/eslint-config",
3
- "version": "6.0.0-next.13",
3
+ "version": "6.0.0-next.15",
4
4
  "description": "ESLint configuration for WebGIS SDK",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,7 +21,7 @@
21
21
  ],
22
22
  "license": "SEE LICENSE IN LICENSE.md",
23
23
  "dependencies": {
24
- "@arcgis/toolkit": "6.0.0-next.13",
24
+ "@arcgis/toolkit": "6.0.0-next.15",
25
25
  "@eslint/markdown": "^8.0.3",
26
26
  "@types/confusing-browser-globals": "^1.0.3",
27
27
  "@typescript-eslint/utils": "^8.63.0",