@checkdigit/eslint-plugin 6.6.0-PR.75-b2d2 → 6.6.0-PR.75-1cd1

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.
@@ -1,4 +1,4 @@
1
- import type { Rule } from 'eslint';
1
+ import { type Rule } from 'eslint';
2
2
  export declare const ruleId = "no-fixture";
3
3
  declare const rule: Rule.RuleModule;
4
4
  export default rule;
package/package.json CHANGED
@@ -1 +1 @@
1
- {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-b2d2","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs && node dist-cjs/index.cjs","build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"6.0.0","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.16.1","@typescript-eslint/parser":"^7.16.1","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
1
+ {"name":"@checkdigit/eslint-plugin","version":"6.6.0-PR.75-1cd1","description":"Check Digit eslint plugins","keywords":["eslint","eslintplugin"],"homepage":"https://github.com/checkdigit/eslint-plugin#readme","bugs":{"url":"https://github.com/checkdigit/eslint-plugin/issues"},"repository":{"type":"git","url":"https://github.com/checkdigit/eslint-plugin"},"license":"MIT","author":"Check Digit, LLC","sideEffects":false,"type":"module","exports":{".":{"types":"./dist-types/index.d.ts","require":"./dist-cjs/index.cjs","import":"./dist-mjs/index.mjs","default":"./dist-mjs/index.mjs"}},"files":["src","dist-types","dist-cjs","dist-mjs","!src/**/*.test.ts","!src/**/*.spec.ts","!dist-types/**/*.test.d.ts","!dist-types/**/*.spec.d.ts","!dist-cjs/**/*.test.cjs","!dist-cjs/**/*.spec.cjs","!dist-mjs/**/*.test.mjs","!dist-mjs/**/*.spec.mjs","SECURITY.md"],"scripts":{"build:dist-cjs":"rimraf dist-cjs && npx builder --type=commonjs --sourceMap --entryPoint=index.ts --outDir=dist-cjs --outFile=index.cjs && echo \"module.exports = module.exports.default;\" >> dist-cjs/index.cjs","build:dist-mjs":"rimraf dist-mjs && npx builder --type=module --sourceMap --outDir=dist-mjs && node dist-mjs/index.mjs","build:dist-types":"rimraf dist-types && npx builder --type=types --outDir=dist-types","ci:compile":"tsc --noEmit","ci:coverage":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=true","ci:lint":"npm run lint","ci:style":"npm run prettier","ci:test":"NODE_OPTIONS=\"--disable-warning ExperimentalWarning --experimental-vm-modules\" jest --coverage=false","lint":"eslint --max-warnings 0 --ignore-path .gitignore .","lint:fix":"eslint --ignore-path .gitignore . --fix","prepublishOnly":"npm run build:dist-types && npm run build:dist-cjs && npm run build:dist-mjs","prettier":"prettier --ignore-path .gitignore --list-different .","prettier:fix":"prettier --ignore-path .gitignore --write .","test":"npm run ci:compile && npm run ci:test && npm run ci:lint && npm run ci:style"},"prettier":"@checkdigit/prettier-config","jest":{"preset":"@checkdigit/jest-config"},"devDependencies":{"@checkdigit/jest-config":"^6.0.2","@checkdigit/prettier-config":"^5.5.0","@checkdigit/typescript-config":"7.1.3-PR.64-9fc8","@types/eslint":"^8.56.10","@typescript-eslint/eslint-plugin":"^7.16.1","@typescript-eslint/parser":"^7.16.1","eslint-config-prettier":"^9.1.0","eslint-plugin-eslint-plugin":"^6.2.0","eslint-plugin-import":"^2.29.1","eslint-plugin-no-only-tests":"^3.1.0","eslint-plugin-no-secrets":"^1.0.2","eslint-plugin-node":"^11.1.0","eslint-plugin-sonarjs":"0.24.0"},"peerDependencies":{"eslint":">=8 <9"},"engines":{"node":">=20.14"}}
package/src/ast/format.ts CHANGED
@@ -13,7 +13,7 @@ import { strict as assert } from 'node:assert';
13
13
  export function getIndentation(node: Node, sourceCode: SourceCode) {
14
14
  assert.ok(node.loc);
15
15
  const line = sourceCode.lines[node.loc.start.line - 1];
16
- assert.ok(line);
16
+ assert.ok(line !== undefined);
17
17
  const indentMatch = line.match(/^\s*/u);
18
18
  return indentMatch ? indentMatch[0] : '';
19
19
  }
package/src/ast/tree.ts CHANGED
@@ -18,14 +18,35 @@ export function getParent(node: Node): Node | undefined | null {
18
18
  return (node as unknown as NodeParentExtension).parent;
19
19
  }
20
20
 
21
- export function getAncestor(node: Node, matcher: string | ((testNode: Node) => boolean), typeToExit?: string) {
21
+ export function getAncestor(
22
+ node: Node,
23
+ matcher: string | ((testNode: Node) => boolean),
24
+ exitMatcher?: string | ((testNode: Node) => boolean),
25
+ ): Node | undefined {
22
26
  const parent = getParent(node);
23
- if (!parent || (typeToExit !== undefined && parent.type === typeToExit)) {
27
+ if (!parent) {
24
28
  return undefined;
25
29
  } else if (typeof matcher === 'string' && parent.type === matcher) {
26
30
  return parent;
27
31
  } else if (typeof matcher === 'function' && matcher(parent)) {
28
32
  return parent;
33
+ } else if (typeof exitMatcher === 'string' && parent.type === exitMatcher) {
34
+ return undefined;
35
+ } else if (typeof exitMatcher === 'function' && exitMatcher(parent)) {
36
+ return undefined;
29
37
  }
30
- return getAncestor(parent, matcher, typeToExit);
38
+ return getAncestor(parent, matcher, exitMatcher);
39
+ }
40
+
41
+ export function getEnclosingStatement(node: Node) {
42
+ return getAncestor(
43
+ node,
44
+ (parentNode) => parentNode.type.endsWith('Statement') || parentNode.type.endsWith('Declaration'),
45
+ );
46
+ }
47
+
48
+ export function getEnclosingScopeNode(node: Node) {
49
+ return getAncestor(node, (parentNode) =>
50
+ ['FunctionExpression', 'FunctionDeclaration', 'ArrowFunctionExpression'].includes(parentNode.type),
51
+ );
31
52
  }
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@
8
8
 
9
9
  import invalidJsonStringify, { ruleId as invalidJsonStringifyRuleId } from './invalid-json-stringify';
10
10
  import noFixture, { ruleId as noFixtureRuleId } from './no-fixture';
11
+ import noFixtureHeaders, { ruleId as noFixtureHeadersRuleId } from './no-fixture-headers';
11
12
  import noPromiseInstanceMethod, { ruleId as noPromiseInstanceMethodRuleId } from './no-promise-instance-method';
12
13
  import filePathComment from './file-path-comment';
13
14
  import noCardNumbers from './no-card-numbers';
@@ -33,6 +34,7 @@ export default {
33
34
  [invalidJsonStringifyRuleId]: invalidJsonStringify,
34
35
  [noPromiseInstanceMethodRuleId]: noPromiseInstanceMethod,
35
36
  [noFixtureRuleId]: noFixture,
37
+ [noFixtureHeadersRuleId]: noFixtureHeaders,
36
38
  },
37
39
  configs: {
38
40
  all: {
@@ -49,6 +51,7 @@ export default {
49
51
  [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error',
50
52
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
51
53
  [`@checkdigit/${noFixtureRuleId}`]: 'error',
54
+ [`@checkdigit/${noFixtureHeadersRuleId}`]: 'error',
52
55
  },
53
56
  },
54
57
  recommended: {
@@ -64,7 +67,6 @@ export default {
64
67
  '@checkdigit/no-test-import': 'error',
65
68
  [`@checkdigit/${invalidJsonStringifyRuleId}`]: 'error',
66
69
  [`@checkdigit/${noPromiseInstanceMethodRuleId}`]: 'error',
67
- [`@checkdigit/${noFixtureRuleId}`]: 'error',
68
70
  },
69
71
  },
70
72
  },
@@ -0,0 +1,134 @@
1
+ // no-fixture.ts
2
+
3
+ /*
4
+ * Copyright (c) 2021-2024 Check Digit, LLC
5
+ *
6
+ * This code is licensed under the MIT license (see LICENSE.txt for details).
7
+ */
8
+
9
+ import type { Identifier, MemberExpression, VariableDeclarator } from 'estree';
10
+ import { getEnclosingScopeNode, getParent } from './ast/tree';
11
+ import { type Rule } from 'eslint';
12
+ import { strict as assert } from 'node:assert';
13
+ import getDocumentationUrl from './get-documentation-url';
14
+
15
+ export const ruleId = 'no-fixture-headers';
16
+
17
+ const rule: Rule.RuleModule = {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description: 'Prefer native fetch API over customized fixture API.',
22
+ url: getDocumentationUrl(ruleId),
23
+ },
24
+ messages: {
25
+ preferNativeFetch: 'Prefer native fetch API over customized fixture API.',
26
+ unknownError:
27
+ 'Unknown error occurred in file "{{fileName}}": {{ error }}. Please manually convert the fixture API call to fetch API call.',
28
+ },
29
+ fixable: 'code',
30
+ schema: [],
31
+ },
32
+ // eslint-disable-next-line max-lines-per-function
33
+ create(context) {
34
+ const sourceCode = context.sourceCode;
35
+ const scopeManager = sourceCode.scopeManager;
36
+
37
+ return {
38
+ // eslint-disable-next-line max-lines-per-function
39
+ 'VariableDeclarator[init.argument.callee.name="fetch"]': (fetchCall: VariableDeclarator) => {
40
+ try {
41
+ const enclosingScopeNode = getEnclosingScopeNode(fetchCall);
42
+ assert.ok(fetchCall.id.type === 'Identifier');
43
+ const fetchVariableName = fetchCall.id.name; /*?*/
44
+ assert.ok(enclosingScopeNode !== undefined, 'enclosing scope node should exist');
45
+ const scope = scopeManager.acquire(enclosingScopeNode);
46
+ const responseVariable = scope?.variables.find((variable) => {
47
+ const identifier = variable.identifiers[0];
48
+ return identifier?.type === 'Identifier' && identifier.name === fetchVariableName;
49
+ });
50
+ if (responseVariable === undefined) {
51
+ return;
52
+ }
53
+
54
+ const headersReferences = responseVariable.references
55
+ .map((reference) => getParent(reference.identifier))
56
+ .filter(
57
+ (parent): parent is MemberExpression =>
58
+ parent?.type === 'MemberExpression' &&
59
+ parent.property.type === 'Identifier' &&
60
+ parent.property.name === 'headers',
61
+ );
62
+ const directHeadersReferences = headersReferences
63
+ .map(getParent)
64
+ .filter(
65
+ (parent): parent is MemberExpression =>
66
+ parent?.type === 'MemberExpression' &&
67
+ !(parent.property.type === 'Identifier' && parent.property.name === 'get'),
68
+ );
69
+ directHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/
70
+
71
+ const reDeclaredHeadersVariableNames = headersReferences
72
+ .map((reference) => getParent(reference))
73
+ .filter((parent): parent is VariableDeclarator => parent?.type === 'VariableDeclarator')
74
+ .map((declarator) => (declarator.id as Identifier).name);
75
+
76
+ const indirectHeadersReferences = reDeclaredHeadersVariableNames
77
+ .map((variableName) => {
78
+ const headersVariable = scope?.variables.find((variable) => {
79
+ const identifier = variable.identifiers[0];
80
+ return identifier?.type === 'Identifier' && identifier.name === variableName;
81
+ });
82
+ return (
83
+ headersVariable?.references
84
+ .map((reference) => getParent(reference.identifier))
85
+ .filter(
86
+ (parent): parent is MemberExpression =>
87
+ parent?.type === 'MemberExpression' &&
88
+ !(parent.property.type === 'Identifier' && parent.property.name === 'get'),
89
+ ) ?? []
90
+ );
91
+ })
92
+ .flat();
93
+ indirectHeadersReferences.map((reference) => sourceCode.getText(reference)); /*?*/
94
+
95
+ const invalidHeadersReferences = [...directHeadersReferences, ...indirectHeadersReferences].map<
96
+ [MemberExpression, string]
97
+ >((reference) => {
98
+ sourceCode.getText(reference); /*?*/
99
+ const headerNameNode = reference.property; /*?*/
100
+ const headerName =
101
+ // eslint-disable-next-line no-nested-ternary, @typescript-eslint/restrict-template-expressions
102
+ reference.computed ? sourceCode.getText(headerNameNode) : `'${sourceCode.getText(headerNameNode)}'`; /*?*/
103
+ const replacementText = `${sourceCode.getText(reference.object)}.get(${headerName})`;
104
+ return [reference, replacementText];
105
+ });
106
+
107
+ context.report({
108
+ node: fetchCall,
109
+ messageId: 'preferNativeFetch',
110
+ *fix(fixer) {
111
+ // handle response headers references
112
+ for (const [node, replacementText] of invalidHeadersReferences) {
113
+ yield fixer.replaceText(node, replacementText);
114
+ }
115
+ },
116
+ });
117
+ } catch (error) {
118
+ // eslint-disable-next-line no-console
119
+ console.error(`Failed to apply ${ruleId} rule for file "${context.filename}":`, error);
120
+ context.report({
121
+ node: fetchCall,
122
+ messageId: 'unknownError',
123
+ data: {
124
+ fileName: context.filename,
125
+ error: error instanceof Error ? error.toString() : JSON.stringify(error),
126
+ },
127
+ });
128
+ }
129
+ },
130
+ };
131
+ },
132
+ };
133
+
134
+ export default rule;