eslint-plugin-traceability 1.4.7 → 1.4.8

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.
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import type { Rule } from "eslint";
7
7
  import { linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory } from "./require-story-io";
8
+ import { getNodeName } from "./require-story-utils";
8
9
  import { DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES } from "./require-story-core";
9
10
  /**
10
11
  * Path to the story file for annotations
@@ -67,14 +68,6 @@ declare function leadingCommentsHasStory(node: any): boolean;
67
68
  * @returns {boolean} true if @story annotation already present
68
69
  */
69
70
  declare function hasStoryAnnotation(sourceCode: any, node: any): boolean;
70
- /**
71
- * Get the name of the function-like node
72
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
73
- * @req REQ-ANNOTATION-REQUIRED - Resolve a human-friendly name for a function-like AST node
74
- * @param {any} node - AST node representing a function-like construct
75
- * @returns {string} the resolved name or "<unknown>"
76
- */
77
- declare function getNodeName(node: any): string;
78
71
  /**
79
72
  * Determine AST node where annotation should be inserted
80
73
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -84,6 +77,15 @@ declare function getNodeName(node: any): string;
84
77
  * @returns {any} AST node that should receive the annotation
85
78
  */
86
79
  declare function resolveTargetNode(sourceCode: any, node: any): any;
80
+ /**
81
+ * Small utility to walk the node and its parents to extract an Identifier or key name.
82
+ * Walks up the parent chain and inspects common properties (id, key, name, Identifier nodes).
83
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
84
+ * @req REQ-ANNOTATION-REQUIRED - Walk node and parents to find Identifier/Key name
85
+ * @param {any} node - AST node to extract a name from
86
+ * @returns {string} extracted name or "(anonymous)" when no name found
87
+ */
88
+ declare function extractName(node: any): string;
87
89
  /**
88
90
  * Check if this node is within scope and matches exportPriority
89
91
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -121,4 +123,4 @@ declare function reportMethod(context: Rule.RuleContext, sourceCode: any, node:
121
123
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
122
124
  * @req REQ-ANNOTATION-REQUIRED - Explicitly export helper functions and constants used by requiring modules
123
125
  */
124
- export { STORY_PATH, ANNOTATION, LOOKBACK_LINES, FALLBACK_WINDOW, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, getNodeName, resolveTargetNode, shouldProcessNode, reportMissing, reportMethod, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, };
126
+ export { STORY_PATH, ANNOTATION, LOOKBACK_LINES, FALLBACK_WINDOW, isExportedNode, jsdocHasStory, commentsBeforeHasStory, leadingCommentsHasStory, hasStoryAnnotation, getNodeName, extractName, resolveTargetNode, shouldProcessNode, reportMissing, reportMethod, DEFAULT_SCOPE, EXPORT_PRIORITY_VALUES, linesBeforeHasStory, parentChainHasStory, fallbackTextBeforeHasStory, };
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.FALLBACK_WINDOW = exports.LOOKBACK_LINES = exports.ANNOTATION = exports.STORY_PATH = void 0;
3
+ exports.fallbackTextBeforeHasStory = exports.parentChainHasStory = exports.linesBeforeHasStory = exports.EXPORT_PRIORITY_VALUES = exports.DEFAULT_SCOPE = exports.getNodeName = exports.FALLBACK_WINDOW = exports.LOOKBACK_LINES = exports.ANNOTATION = exports.STORY_PATH = void 0;
4
4
  exports.isExportedNode = isExportedNode;
5
5
  exports.jsdocHasStory = jsdocHasStory;
6
6
  exports.commentsBeforeHasStory = commentsBeforeHasStory;
7
7
  exports.leadingCommentsHasStory = leadingCommentsHasStory;
8
8
  exports.hasStoryAnnotation = hasStoryAnnotation;
9
- exports.getNodeName = getNodeName;
9
+ exports.extractName = extractName;
10
10
  exports.resolveTargetNode = resolveTargetNode;
11
11
  exports.shouldProcessNode = shouldProcessNode;
12
12
  exports.reportMissing = reportMissing;
@@ -15,6 +15,8 @@ const require_story_io_1 = require("./require-story-io");
15
15
  Object.defineProperty(exports, "linesBeforeHasStory", { enumerable: true, get: function () { return require_story_io_1.linesBeforeHasStory; } });
16
16
  Object.defineProperty(exports, "parentChainHasStory", { enumerable: true, get: function () { return require_story_io_1.parentChainHasStory; } });
17
17
  Object.defineProperty(exports, "fallbackTextBeforeHasStory", { enumerable: true, get: function () { return require_story_io_1.fallbackTextBeforeHasStory; } });
18
+ const require_story_utils_1 = require("./require-story-utils");
19
+ Object.defineProperty(exports, "getNodeName", { enumerable: true, get: function () { return require_story_utils_1.getNodeName; } });
18
20
  const require_story_core_1 = require("./require-story-core");
19
21
  Object.defineProperty(exports, "DEFAULT_SCOPE", { enumerable: true, get: function () { return require_story_core_1.DEFAULT_SCOPE; } });
20
22
  Object.defineProperty(exports, "EXPORT_PRIORITY_VALUES", { enumerable: true, get: function () { return require_story_core_1.EXPORT_PRIORITY_VALUES; } });
@@ -139,37 +141,6 @@ function hasStoryAnnotation(sourceCode, node) {
139
141
  }
140
142
  return false;
141
143
  }
142
- /**
143
- * Get the name of the function-like node
144
- * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
145
- * @req REQ-ANNOTATION-REQUIRED - Resolve a human-friendly name for a function-like AST node
146
- * @param {any} node - AST node representing a function-like construct
147
- * @returns {string} the resolved name or "<unknown>"
148
- */
149
- function getNodeName(node) {
150
- let current = node;
151
- while (current) {
152
- if (current.type === "VariableDeclarator" &&
153
- current.id &&
154
- typeof current.id.name === "string") {
155
- return current.id.name;
156
- }
157
- if ((current.type === "FunctionDeclaration" ||
158
- current.type === "TSDeclareFunction") &&
159
- current.id &&
160
- typeof current.id.name === "string") {
161
- return current.id.name;
162
- }
163
- if ((current.type === "MethodDefinition" ||
164
- current.type === "TSMethodSignature") &&
165
- current.key &&
166
- typeof current.key.name === "string") {
167
- return current.key.name;
168
- }
169
- current = current.parent;
170
- }
171
- return "<unknown>";
172
- }
173
144
  /**
174
145
  * Determine AST node where annotation should be inserted
175
146
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -202,6 +173,45 @@ function resolveTargetNode(sourceCode, node) {
202
173
  }
203
174
  return node;
204
175
  }
176
+ /**
177
+ * Small utility to walk the node and its parents to extract an Identifier or key name.
178
+ * Walks up the parent chain and inspects common properties (id, key, name, Identifier nodes).
179
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
180
+ * @req REQ-ANNOTATION-REQUIRED - Walk node and parents to find Identifier/Key name
181
+ * @param {any} node - AST node to extract a name from
182
+ * @returns {string} extracted name or "(anonymous)" when no name found
183
+ */
184
+ function extractName(node) {
185
+ let n = node;
186
+ while (n) {
187
+ // Direct Identifier node
188
+ if (n.type === "Identifier" && typeof n.name === "string") {
189
+ return n.name;
190
+ }
191
+ // id property (FunctionDeclaration, etc.)
192
+ if (n.id && n.id.type === "Identifier" && typeof n.id.name === "string") {
193
+ return n.id.name;
194
+ }
195
+ // key property (Property, MethodDefinition, etc.)
196
+ if (n.key &&
197
+ n.key.type === "Identifier" &&
198
+ typeof n.key.name === "string") {
199
+ return n.key.name;
200
+ }
201
+ // name property (some nodes may have a 'name' string directly)
202
+ if (typeof n.name === "string" && n.name.length > 0) {
203
+ return n.name;
204
+ }
205
+ // computed keys may have an Identifier inside
206
+ if (n.key &&
207
+ n.key.type === "Literal" &&
208
+ typeof n.key.value === "string") {
209
+ return n.key.value;
210
+ }
211
+ n = n.parent;
212
+ }
213
+ return "(anonymous)";
214
+ }
205
215
  /**
206
216
  * Check if this node is within scope and matches exportPriority
207
217
  * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
@@ -236,7 +246,7 @@ function shouldProcessNode(node, scope, exportPriority = "all") {
236
246
  */
237
247
  function reportMissing(context, sourceCode, node, passedTarget) {
238
248
  try {
239
- const functionName = getNodeName(node);
249
+ const functionName = extractName(node && (node.id || node.key) ? node.id || node.key : node);
240
250
  if (hasStoryAnnotation(sourceCode, node)) {
241
251
  return;
242
252
  }
@@ -274,7 +284,7 @@ function reportMethod(context, sourceCode, node, passedTarget) {
274
284
  return;
275
285
  }
276
286
  const resolvedTarget = passedTarget ?? resolveTargetNode(sourceCode, node);
277
- const name = getNodeName(node);
287
+ const name = extractName(node);
278
288
  context.report({
279
289
  node,
280
290
  messageId: "missingStory",
@@ -0,0 +1,34 @@
1
+ /**
2
+ * src/rules/helpers/require-story-utils.ts
3
+ *
4
+ * Utility: getNodeName
5
+ *
6
+ * Traceability:
7
+ * - File: src/rules/helpers/require-story-utils.ts
8
+ * - Purpose: Helper used by rules to obtain a readable "name" from various AST node shapes.
9
+ * - Created to centralize logic for extracting identifier/property/literal names from ESTree / TSESTree nodes.
10
+ *
11
+ * Docs/Annotations:
12
+ * - Story: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
13
+ * - Requirement annotation: REQ-ANNOTATION-REQUIRED
14
+ *
15
+ * Behavior:
16
+ * - Accepts common AST node shapes (Identifier, Literal, Property, MemberExpression, TemplateLiteral, JSXIdentifier, etc.)
17
+ * - Returns a string name when it can be reasonably determined, otherwise returns null.
18
+ *
19
+ * Notes:
20
+ * - This helper is intentionally defensive and conservative: when the node is computed or contains expressions
21
+ * that cannot be resolved statically, it returns null.
22
+ * - The function uses weak typing for the node parameter to maximize compatibility with different AST types
23
+ * (ESTree, TSESTree, JSX variations).
24
+ */
25
+ /**
26
+ * Get a readable name for a given AST node.
27
+ *
28
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
29
+ * @req REQ-ANNOTATION-REQUIRED
30
+ *
31
+ * @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
32
+ * @returns The resolved name string, or null if a stable name cannot be determined.
33
+ */
34
+ export declare function getNodeName(node: any): string | null;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /**
3
+ * src/rules/helpers/require-story-utils.ts
4
+ *
5
+ * Utility: getNodeName
6
+ *
7
+ * Traceability:
8
+ * - File: src/rules/helpers/require-story-utils.ts
9
+ * - Purpose: Helper used by rules to obtain a readable "name" from various AST node shapes.
10
+ * - Created to centralize logic for extracting identifier/property/literal names from ESTree / TSESTree nodes.
11
+ *
12
+ * Docs/Annotations:
13
+ * - Story: docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
14
+ * - Requirement annotation: REQ-ANNOTATION-REQUIRED
15
+ *
16
+ * Behavior:
17
+ * - Accepts common AST node shapes (Identifier, Literal, Property, MemberExpression, TemplateLiteral, JSXIdentifier, etc.)
18
+ * - Returns a string name when it can be reasonably determined, otherwise returns null.
19
+ *
20
+ * Notes:
21
+ * - This helper is intentionally defensive and conservative: when the node is computed or contains expressions
22
+ * that cannot be resolved statically, it returns null.
23
+ * - The function uses weak typing for the node parameter to maximize compatibility with different AST types
24
+ * (ESTree, TSESTree, JSX variations).
25
+ */
26
+ Object.defineProperty(exports, "__esModule", { value: true });
27
+ exports.getNodeName = getNodeName;
28
+ /**
29
+ * Check for identifier-like nodes and return their name when available.
30
+ *
31
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
32
+ * @req REQ-ANNOTATION-REQUIRED
33
+ *
34
+ * @param node - AST node to inspect
35
+ * @returns the identifier name or null
36
+ */
37
+ function isIdentifierLike(node) {
38
+ if (!node)
39
+ return null;
40
+ const type = node.type;
41
+ if (type === "Identifier" || type === "JSXIdentifier") {
42
+ return typeof node.name === "string" ? node.name : null;
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Convert a Literal node to a string when it represents a stable primitive (string/number/boolean).
48
+ *
49
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
50
+ * @req REQ-ANNOTATION-REQUIRED
51
+ *
52
+ * @param node - AST node expected to be a Literal
53
+ * @returns the literal as string or null if not stable/resolvable
54
+ */
55
+ function literalToString(node) {
56
+ if (!node || node.type !== "Literal")
57
+ return null;
58
+ const val = node.value;
59
+ if (val === null)
60
+ return null;
61
+ const t = typeof val;
62
+ return t === "string" || t === "number" || t === "boolean"
63
+ ? String(val)
64
+ : null;
65
+ }
66
+ /**
67
+ * Convert a TemplateLiteral node to a string if it contains no expressions.
68
+ *
69
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
70
+ * @req REQ-ANNOTATION-REQUIRED
71
+ *
72
+ * @param node - AST node expected to be a TemplateLiteral
73
+ * @returns the cooked/raw concatenated template string or null if it contains expressions
74
+ */
75
+ function templateLiteralToString(node) {
76
+ if (!node || node.type !== "TemplateLiteral")
77
+ return null;
78
+ const expressions = node.expressions || [];
79
+ if (expressions.length !== 0)
80
+ return null;
81
+ const quasis = node.quasis || [];
82
+ return quasis
83
+ .map((q) => {
84
+ if (!q || !q.value)
85
+ return "";
86
+ if (typeof q.value.cooked === "string")
87
+ return q.value.cooked;
88
+ if (typeof q.value.raw === "string")
89
+ return q.value.raw;
90
+ return "";
91
+ })
92
+ .join("");
93
+ }
94
+ /**
95
+ * Resolve a MemberExpression / TSQualifiedName / JSXMemberExpression-like node to a name when non-computed.
96
+ *
97
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
98
+ * @req REQ-ANNOTATION-REQUIRED
99
+ *
100
+ * @param node - AST node to inspect
101
+ * @returns resolved member/property name or null
102
+ */
103
+ function memberExpressionName(node) {
104
+ if (!node)
105
+ return null;
106
+ const type = node.type;
107
+ if (type !== "MemberExpression" &&
108
+ type !== "TSQualifiedName" &&
109
+ type !== "JSXMemberExpression") {
110
+ return null;
111
+ }
112
+ // For TSQualifiedName and JSXMemberExpression, treat like non-computed access.
113
+ if (type === "TSQualifiedName" ||
114
+ type === "JSXMemberExpression" ||
115
+ node.computed === false) {
116
+ return getNodeName(node.property || node.right);
117
+ }
118
+ return null;
119
+ }
120
+ /**
121
+ * Extract the key name from Property/ObjectProperty nodes.
122
+ *
123
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
124
+ * @req REQ-ANNOTATION-REQUIRED
125
+ *
126
+ * @param node - AST node expected to be Property/ObjectProperty
127
+ * @returns the resolved key name or null
128
+ */
129
+ function propertyKeyName(node) {
130
+ if (!node)
131
+ return null;
132
+ if (node.type === "Property" || node.type === "ObjectProperty") {
133
+ return getNodeName(node.key);
134
+ }
135
+ return null;
136
+ }
137
+ /**
138
+ * Extract direct-level names from common container fields (.id / .key) to reduce branching in getNodeName.
139
+ *
140
+ * Branch-level traceability: prefer direct .id.name when available (common on function/class declarations)
141
+ * Branch-level traceability: prefer direct .key.name early (common on variable declarators, properties)
142
+ *
143
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
144
+ * @req REQ-ANNOTATION-REQUIRED
145
+ *
146
+ * @param node - AST node to inspect for direct .id/.key name
147
+ * @returns the resolved direct name or null
148
+ */
149
+ function directName(node) {
150
+ if (!node)
151
+ return null;
152
+ // Prefer direct .id.name when available
153
+ if (node.id && typeof node.id.name === "string") {
154
+ return node.id.name;
155
+ }
156
+ if (node.id) {
157
+ const idName = getNodeName(node.id);
158
+ if (idName !== null)
159
+ return idName;
160
+ }
161
+ // Prefer direct .key.name early
162
+ if (node.key && typeof node.key.name === "string") {
163
+ return node.key.name;
164
+ }
165
+ if (node.key) {
166
+ const keyName = getNodeName(node.key);
167
+ if (keyName !== null)
168
+ return keyName;
169
+ }
170
+ return null;
171
+ }
172
+ /**
173
+ * Get a readable name for a given AST node.
174
+ *
175
+ * @story docs/stories/003.0-DEV-FUNCTION-ANNOTATIONS.story.md
176
+ * @req REQ-ANNOTATION-REQUIRED
177
+ *
178
+ * @param node - An AST node (ESTree/TSESTree/JSX-like). Can be null/undefined.
179
+ * @returns The resolved name string, or null if a stable name cannot be determined.
180
+ */
181
+ function getNodeName(node) {
182
+ if (!node)
183
+ return null;
184
+ // Delegate direct-level id/key resolution to helper to reduce cyclomatic complexity
185
+ const direct = directName(node);
186
+ if (direct !== null)
187
+ return direct;
188
+ // Identifier-like nodes
189
+ const idName = isIdentifierLike(node);
190
+ if (idName !== null)
191
+ return idName;
192
+ // Literal nodes
193
+ const lit = literalToString(node);
194
+ if (lit !== null)
195
+ return lit;
196
+ // TemplateLiteral nodes
197
+ const tpl = templateLiteralToString(node);
198
+ if (tpl !== null)
199
+ return tpl;
200
+ // Property-like nodes
201
+ const prop = propertyKeyName(node);
202
+ if (prop !== null)
203
+ return prop;
204
+ // Member/qualified/member-expression-like nodes
205
+ const member = memberExpressionName(node);
206
+ if (member !== null)
207
+ return member;
208
+ // TypeScript literal wrapper
209
+ if (node.type === "TSLiteralType" && node.literal) {
210
+ return getNodeName(node.literal);
211
+ }
212
+ // JSX namespaced name
213
+ if (node.type === "JSXNamespacedName") {
214
+ return getNodeName(node.name);
215
+ }
216
+ // Generic fallback: try .key if present on other shapes
217
+ if (node.key) {
218
+ return getNodeName(node.key);
219
+ }
220
+ return null;
221
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-traceability",
3
- "version": "1.4.7",
3
+ "version": "1.4.8",
4
4
  "description": "A customizable ESLint plugin that enforces traceability annotations in your code, ensuring each implementation is linked to its requirement or test case.",
5
5
  "main": "lib/src/index.js",
6
6
  "types": "lib/src/index.d.ts",
@@ -22,7 +22,7 @@
22
22
  "lint": "eslint --config eslint.config.js \"src/**/*.{js,ts}\" \"tests/**/*.{js,ts}\" --max-warnings=0",
23
23
  "test": "jest --ci --bail",
24
24
  "ci-verify": "npm run type-check && npm run lint && npm run format:check && npm run duplication && npm run check:traceability && npm test && npm run audit:ci && npm run safety:deps",
25
- "ci-verify:fast": "npm run lint:require-built-plugin && npm run type-check && npm run check:traceability && npm run duplication && jest --ci --bail --passWithNoTests --testPathPatterns 'tests/(unit|fast)'",
25
+ "ci-verify:fast": "npm run type-check && npm run check:traceability && npm run duplication && jest --ci --bail --passWithNoTests --testPathPatterns 'tests/(unit|fast)'",
26
26
  "format": "prettier --write .",
27
27
  "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"",
28
28
  "duplication": "jscpd src tests --reporters console --threshold 3 --ignore tests/utils/**",