@optave/codegraph 3.1.5 → 3.2.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 (91) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -7
  3. package/src/ast-analysis/engine.js +252 -258
  4. package/src/ast-analysis/shared.js +0 -12
  5. package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
  6. package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
  7. package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
  8. package/src/cli/commands/ast.js +2 -1
  9. package/src/cli/commands/audit.js +2 -1
  10. package/src/cli/commands/batch.js +2 -1
  11. package/src/cli/commands/brief.js +12 -0
  12. package/src/cli/commands/cfg.js +2 -1
  13. package/src/cli/commands/check.js +20 -23
  14. package/src/cli/commands/children.js +6 -1
  15. package/src/cli/commands/complexity.js +2 -1
  16. package/src/cli/commands/context.js +6 -1
  17. package/src/cli/commands/dataflow.js +2 -1
  18. package/src/cli/commands/deps.js +8 -3
  19. package/src/cli/commands/flow.js +2 -1
  20. package/src/cli/commands/fn-impact.js +6 -1
  21. package/src/cli/commands/owners.js +4 -2
  22. package/src/cli/commands/query.js +6 -1
  23. package/src/cli/commands/roles.js +2 -1
  24. package/src/cli/commands/search.js +8 -2
  25. package/src/cli/commands/sequence.js +2 -1
  26. package/src/cli/commands/triage.js +38 -27
  27. package/src/db/connection.js +18 -12
  28. package/src/db/migrations.js +41 -64
  29. package/src/db/query-builder.js +60 -4
  30. package/src/db/repository/in-memory-repository.js +27 -16
  31. package/src/db/repository/nodes.js +8 -10
  32. package/src/domain/analysis/brief.js +155 -0
  33. package/src/domain/analysis/context.js +174 -190
  34. package/src/domain/analysis/dependencies.js +200 -146
  35. package/src/domain/analysis/exports.js +3 -2
  36. package/src/domain/analysis/impact.js +267 -152
  37. package/src/domain/analysis/module-map.js +247 -221
  38. package/src/domain/analysis/roles.js +8 -5
  39. package/src/domain/analysis/symbol-lookup.js +7 -5
  40. package/src/domain/graph/builder/helpers.js +1 -1
  41. package/src/domain/graph/builder/incremental.js +116 -90
  42. package/src/domain/graph/builder/pipeline.js +106 -80
  43. package/src/domain/graph/builder/stages/build-edges.js +318 -239
  44. package/src/domain/graph/builder/stages/detect-changes.js +198 -177
  45. package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
  46. package/src/domain/graph/watcher.js +2 -2
  47. package/src/domain/parser.js +20 -11
  48. package/src/domain/queries.js +1 -0
  49. package/src/domain/search/search/filters.js +9 -5
  50. package/src/domain/search/search/keyword.js +12 -5
  51. package/src/domain/search/search/prepare.js +13 -5
  52. package/src/extractors/csharp.js +224 -207
  53. package/src/extractors/go.js +176 -172
  54. package/src/extractors/hcl.js +94 -78
  55. package/src/extractors/java.js +213 -207
  56. package/src/extractors/javascript.js +274 -304
  57. package/src/extractors/php.js +234 -221
  58. package/src/extractors/python.js +252 -250
  59. package/src/extractors/ruby.js +192 -185
  60. package/src/extractors/rust.js +182 -167
  61. package/src/features/ast.js +5 -3
  62. package/src/features/audit.js +4 -2
  63. package/src/features/boundaries.js +98 -83
  64. package/src/features/cfg.js +134 -143
  65. package/src/features/communities.js +68 -53
  66. package/src/features/complexity.js +143 -132
  67. package/src/features/dataflow.js +146 -149
  68. package/src/features/export.js +3 -3
  69. package/src/features/graph-enrichment.js +2 -2
  70. package/src/features/manifesto.js +9 -6
  71. package/src/features/owners.js +4 -3
  72. package/src/features/sequence.js +152 -141
  73. package/src/features/shared/find-nodes.js +31 -0
  74. package/src/features/structure.js +130 -99
  75. package/src/features/triage.js +83 -68
  76. package/src/graph/classifiers/risk.js +3 -2
  77. package/src/graph/classifiers/roles.js +6 -3
  78. package/src/index.js +1 -0
  79. package/src/mcp/server.js +65 -56
  80. package/src/mcp/tool-registry.js +13 -0
  81. package/src/mcp/tools/brief.js +8 -0
  82. package/src/mcp/tools/index.js +2 -0
  83. package/src/presentation/brief.js +51 -0
  84. package/src/presentation/queries-cli/exports.js +21 -14
  85. package/src/presentation/queries-cli/impact.js +55 -39
  86. package/src/presentation/queries-cli/inspect.js +184 -189
  87. package/src/presentation/queries-cli/overview.js +57 -58
  88. package/src/presentation/queries-cli/path.js +36 -29
  89. package/src/presentation/table.js +0 -8
  90. package/src/shared/generators.js +7 -3
  91. package/src/shared/kinds.js +1 -1
@@ -21,9 +21,10 @@ import { walkWithVisitors } from '../ast-analysis/visitor.js';
21
21
  import { createDataflowVisitor } from '../ast-analysis/visitors/dataflow-visitor.js';
22
22
  import { hasDataflowTable, openReadonlyOrFail } from '../db/index.js';
23
23
  import { ALL_SYMBOL_KINDS, normalizeSymbol } from '../domain/queries.js';
24
- import { info } from '../infrastructure/logger.js';
24
+ import { debug, info } from '../infrastructure/logger.js';
25
25
  import { isTestFile } from '../infrastructure/test-filter.js';
26
26
  import { paginateResult } from '../shared/paginate.js';
27
+ import { findNodes } from './shared/find-nodes.js';
27
28
 
28
29
  // Re-export for backward compatibility
29
30
  export { _makeDataflowRules as makeDataflowRules, DATAFLOW_RULES };
@@ -57,26 +58,11 @@ export function extractDataflow(tree, _filePath, _definitions, langId = 'javascr
57
58
  return results.dataflow;
58
59
  }
59
60
 
60
- // ── buildDataflowEdges ──────────────────────────────────────────────────────
61
+ // ── Build-Time Helpers ──────────────────────────────────────────────────────
61
62
 
62
- /**
63
- * Build dataflow edges and insert them into the database.
64
- * Called during graph build when --dataflow is enabled.
65
- *
66
- * @param {object} db - better-sqlite3 database instance
67
- * @param {Map<string, object>} fileSymbols - map of relPath → symbols
68
- * @param {string} rootDir - absolute root directory
69
- * @param {object} engineOpts - engine options
70
- */
71
- export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) {
72
- // Lazily init WASM parsers if needed
73
- let parsers = null;
63
+ async function initDataflowParsers(fileSymbols) {
74
64
  let needsFallback = false;
75
65
 
76
- // Always build ext→langId map so native-only builds (where _langId is unset)
77
- // can still derive the language from the file extension.
78
- const extToLang = buildExtToLangMap();
79
-
80
66
  for (const [relPath, symbols] of fileSymbols) {
81
67
  if (!symbols._tree && !symbols.dataflow) {
82
68
  const ext = path.extname(relPath).toLowerCase();
@@ -87,25 +73,130 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
87
73
  }
88
74
  }
89
75
 
76
+ let parsers = null;
77
+ let getParserFn = null;
78
+
90
79
  if (needsFallback) {
91
80
  const { createParsers } = await import('../domain/parser.js');
92
81
  parsers = await createParsers();
93
- }
94
-
95
- let getParserFn = null;
96
- if (parsers) {
97
82
  const mod = await import('../domain/parser.js');
98
83
  getParserFn = mod.getParser;
99
84
  }
100
85
 
86
+ return { parsers, getParserFn };
87
+ }
88
+
89
+ function getDataflowForFile(symbols, relPath, rootDir, extToLang, parsers, getParserFn) {
90
+ if (symbols.dataflow) return symbols.dataflow;
91
+
92
+ let tree = symbols._tree;
93
+ let langId = symbols._langId;
94
+
95
+ if (!tree) {
96
+ if (!getParserFn) return null;
97
+ const ext = path.extname(relPath).toLowerCase();
98
+ langId = extToLang.get(ext);
99
+ if (!langId || !DATAFLOW_RULES.has(langId)) return null;
100
+
101
+ const absPath = path.join(rootDir, relPath);
102
+ let code;
103
+ try {
104
+ code = fs.readFileSync(absPath, 'utf-8');
105
+ } catch (e) {
106
+ debug(`dataflow: cannot read ${relPath}: ${e.message}`);
107
+ return null;
108
+ }
109
+
110
+ const parser = getParserFn(parsers, absPath);
111
+ if (!parser) return null;
112
+
113
+ try {
114
+ tree = parser.parse(code);
115
+ } catch (e) {
116
+ debug(`dataflow: parse failed for ${relPath}: ${e.message}`);
117
+ return null;
118
+ }
119
+ }
120
+
121
+ if (!langId) {
122
+ const ext = path.extname(relPath).toLowerCase();
123
+ langId = extToLang.get(ext);
124
+ if (!langId) return null;
125
+ }
126
+
127
+ if (!DATAFLOW_RULES.has(langId)) return null;
128
+
129
+ return extractDataflow(tree, relPath, symbols.definitions, langId);
130
+ }
131
+
132
+ function insertDataflowEdges(insert, data, resolveNode) {
133
+ let edgeCount = 0;
134
+
135
+ for (const flow of data.argFlows) {
136
+ const sourceNode = resolveNode(flow.callerFunc);
137
+ const targetNode = resolveNode(flow.calleeName);
138
+ if (sourceNode && targetNode) {
139
+ insert.run(
140
+ sourceNode.id,
141
+ targetNode.id,
142
+ 'flows_to',
143
+ flow.argIndex,
144
+ flow.expression,
145
+ flow.line,
146
+ flow.confidence,
147
+ );
148
+ edgeCount++;
149
+ }
150
+ }
151
+
152
+ for (const assignment of data.assignments) {
153
+ const producerNode = resolveNode(assignment.sourceCallName);
154
+ const consumerNode = resolveNode(assignment.callerFunc);
155
+ if (producerNode && consumerNode) {
156
+ insert.run(
157
+ producerNode.id,
158
+ consumerNode.id,
159
+ 'returns',
160
+ null,
161
+ assignment.expression,
162
+ assignment.line,
163
+ 1.0,
164
+ );
165
+ edgeCount++;
166
+ }
167
+ }
168
+
169
+ for (const mut of data.mutations) {
170
+ const mutatorNode = resolveNode(mut.funcName);
171
+ if (mutatorNode && mut.binding?.type === 'param') {
172
+ insert.run(mutatorNode.id, mutatorNode.id, 'mutates', null, mut.mutatingExpr, mut.line, 1.0);
173
+ edgeCount++;
174
+ }
175
+ }
176
+
177
+ return edgeCount;
178
+ }
179
+
180
+ // ── buildDataflowEdges ──────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * Build dataflow edges and insert them into the database.
184
+ * Called during graph build when --dataflow is enabled.
185
+ *
186
+ * @param {object} db - better-sqlite3 database instance
187
+ * @param {Map<string, object>} fileSymbols - map of relPath → symbols
188
+ * @param {string} rootDir - absolute root directory
189
+ * @param {object} engineOpts - engine options
190
+ */
191
+ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts) {
192
+ const extToLang = buildExtToLangMap();
193
+ const { parsers, getParserFn } = await initDataflowParsers(fileSymbols);
194
+
101
195
  const insert = db.prepare(
102
196
  `INSERT INTO dataflow (source_id, target_id, kind, param_index, expression, line, confidence)
103
197
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
104
198
  );
105
199
 
106
- // MVP scope: only resolve function/method nodes for dataflow edges.
107
- // Future expansion: add 'parameter', 'property', 'constant' kinds to track
108
- // data flow through property accessors or constant references.
109
200
  const getNodeByNameAndFile = db.prepare(
110
201
  `SELECT id, name, kind, file, line FROM nodes
111
202
  WHERE name = ? AND file = ? AND kind IN ('function', 'method')`,
@@ -124,107 +215,17 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
124
215
  const ext = path.extname(relPath).toLowerCase();
125
216
  if (!DATAFLOW_EXTENSIONS.has(ext)) continue;
126
217
 
127
- // Use native dataflow data if available skip WASM extraction
128
- let data = symbols.dataflow;
129
- if (!data) {
130
- let tree = symbols._tree;
131
- let langId = symbols._langId;
132
-
133
- // WASM fallback if no cached tree
134
- if (!tree) {
135
- if (!getParserFn) continue;
136
- langId = extToLang.get(ext);
137
- if (!langId || !DATAFLOW_RULES.has(langId)) continue;
138
-
139
- const absPath = path.join(rootDir, relPath);
140
- let code;
141
- try {
142
- code = fs.readFileSync(absPath, 'utf-8');
143
- } catch {
144
- continue;
145
- }
146
-
147
- const parser = getParserFn(parsers, absPath);
148
- if (!parser) continue;
149
-
150
- try {
151
- tree = parser.parse(code);
152
- } catch {
153
- continue;
154
- }
155
- }
156
-
157
- if (!langId) {
158
- langId = extToLang.get(ext);
159
- if (!langId) continue;
160
- }
161
-
162
- if (!DATAFLOW_RULES.has(langId)) continue;
218
+ const data = getDataflowForFile(symbols, relPath, rootDir, extToLang, parsers, getParserFn);
219
+ if (!data) continue;
163
220
 
164
- data = extractDataflow(tree, relPath, symbols.definitions, langId);
165
- }
166
-
167
- // Resolve function names to node IDs in this file first, then globally
168
- function resolveNode(funcName) {
221
+ const resolveNode = (funcName) => {
169
222
  const local = getNodeByNameAndFile.all(funcName, relPath);
170
223
  if (local.length > 0) return local[0];
171
224
  const global = getNodeByName.all(funcName);
172
225
  return global.length > 0 ? global[0] : null;
173
- }
174
-
175
- // flows_to: parameter/variable passed as argument to another function
176
- for (const flow of data.argFlows) {
177
- const sourceNode = resolveNode(flow.callerFunc);
178
- const targetNode = resolveNode(flow.calleeName);
179
- if (sourceNode && targetNode) {
180
- insert.run(
181
- sourceNode.id,
182
- targetNode.id,
183
- 'flows_to',
184
- flow.argIndex,
185
- flow.expression,
186
- flow.line,
187
- flow.confidence,
188
- );
189
- totalEdges++;
190
- }
191
- }
192
-
193
- // returns: call return value captured in caller
194
- for (const assignment of data.assignments) {
195
- const producerNode = resolveNode(assignment.sourceCallName);
196
- const consumerNode = resolveNode(assignment.callerFunc);
197
- if (producerNode && consumerNode) {
198
- insert.run(
199
- producerNode.id,
200
- consumerNode.id,
201
- 'returns',
202
- null,
203
- assignment.expression,
204
- assignment.line,
205
- 1.0,
206
- );
207
- totalEdges++;
208
- }
209
- }
226
+ };
210
227
 
211
- // mutates: parameter-derived value is mutated
212
- for (const mut of data.mutations) {
213
- const mutatorNode = resolveNode(mut.funcName);
214
- if (mutatorNode && mut.binding?.type === 'param') {
215
- // The mutation in this function affects the parameter source
216
- insert.run(
217
- mutatorNode.id,
218
- mutatorNode.id,
219
- 'mutates',
220
- null,
221
- mut.mutatingExpr,
222
- mut.line,
223
- 1.0,
224
- );
225
- totalEdges++;
226
- }
227
- }
228
+ totalEdges += insertDataflowEdges(insert, data, resolveNode);
228
229
  }
229
230
  });
230
231
 
@@ -234,31 +235,7 @@ export async function buildDataflowEdges(db, fileSymbols, rootDir, _engineOpts)
234
235
 
235
236
  // ── Query functions ─────────────────────────────────────────────────────────
236
237
 
237
- /**
238
- * Look up node(s) by name with optional file/kind/noTests filtering.
239
- * Similar to findMatchingNodes in queries.js but operates on the dataflow table.
240
- */
241
- function findNodes(db, name, opts = {}) {
242
- const kinds = opts.kind ? [opts.kind] : ALL_SYMBOL_KINDS;
243
- const placeholders = kinds.map(() => '?').join(', ');
244
- const params = [`%${name}%`, ...kinds];
245
-
246
- let fileCondition = '';
247
- if (opts.file) {
248
- fileCondition = ' AND file LIKE ?';
249
- params.push(`%${opts.file}%`);
250
- }
251
-
252
- const rows = db
253
- .prepare(
254
- `SELECT * FROM nodes
255
- WHERE name LIKE ? AND kind IN (${placeholders})${fileCondition}
256
- ORDER BY file, line`,
257
- )
258
- .all(...params);
259
-
260
- return opts.noTests ? rows.filter((n) => !isTestFile(n.file)) : rows;
261
- }
238
+ // findNodes imported from ./shared/find-nodes.js
262
239
 
263
240
  /**
264
241
  * Return all dataflow edges for a symbol.
@@ -282,7 +259,12 @@ export function dataflowData(name, customDbPath, opts = {}) {
282
259
  };
283
260
  }
284
261
 
285
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
262
+ const nodes = findNodes(
263
+ db,
264
+ name,
265
+ { noTests, file: opts.file, kind: opts.kind },
266
+ ALL_SYMBOL_KINDS,
267
+ );
286
268
  if (nodes.length === 0) {
287
269
  return { name, results: [] };
288
270
  }
@@ -426,12 +408,22 @@ export function dataflowPathData(from, to, customDbPath, opts = {}) {
426
408
  };
427
409
  }
428
410
 
429
- const fromNodes = findNodes(db, from, { noTests, file: opts.fromFile, kind: opts.kind });
411
+ const fromNodes = findNodes(
412
+ db,
413
+ from,
414
+ { noTests, file: opts.fromFile, kind: opts.kind },
415
+ ALL_SYMBOL_KINDS,
416
+ );
430
417
  if (fromNodes.length === 0) {
431
418
  return { from, to, found: false, error: `No symbol matching "${from}"` };
432
419
  }
433
420
 
434
- const toNodes = findNodes(db, to, { noTests, file: opts.toFile, kind: opts.kind });
421
+ const toNodes = findNodes(
422
+ db,
423
+ to,
424
+ { noTests, file: opts.toFile, kind: opts.kind },
425
+ ALL_SYMBOL_KINDS,
426
+ );
435
427
  if (toNodes.length === 0) {
436
428
  return { from, to, found: false, error: `No symbol matching "${to}"` };
437
429
  }
@@ -554,7 +546,12 @@ export function dataflowImpactData(name, customDbPath, opts = {}) {
554
546
  };
555
547
  }
556
548
 
557
- const nodes = findNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
549
+ const nodes = findNodes(
550
+ db,
551
+ name,
552
+ { noTests, file: opts.file, kind: opts.kind },
553
+ ALL_SYMBOL_KINDS,
554
+ );
558
555
  if (nodes.length === 0) {
559
556
  return { name, results: [] };
560
557
  }
@@ -67,8 +67,8 @@ function loadFunctionLevelEdges(db, { noTests, minConfidence, limit }) {
67
67
  FROM edges e
68
68
  JOIN nodes n1 ON e.source_id = n1.id
69
69
  JOIN nodes n2 ON e.target_id = n2.id
70
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
71
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
70
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
71
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
72
72
  AND e.kind = 'calls'
73
73
  AND e.confidence >= ?
74
74
  `,
@@ -308,7 +308,7 @@ export function exportGraphSON(db, opts = {}) {
308
308
  let nodes = db
309
309
  .prepare(`
310
310
  SELECT id, name, kind, file, line, role FROM nodes
311
- WHERE kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'file')
311
+ WHERE kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant', 'file')
312
312
  `)
313
313
  .all();
314
314
  if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
@@ -42,8 +42,8 @@ function prepareFunctionLevelData(db, noTests, minConf, cfg) {
42
42
  FROM edges e
43
43
  JOIN nodes n1 ON e.source_id = n1.id
44
44
  JOIN nodes n2 ON e.target_id = n2.id
45
- WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
46
- AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
45
+ WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
46
+ AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')
47
47
  AND e.kind = 'calls'
48
48
  AND e.confidence >= ?
49
49
  `,
@@ -1,4 +1,5 @@
1
1
  import { openReadonlyOrFail } from '../db/index.js';
2
+ import { buildFileConditionSQL } from '../db/query-builder.js';
2
3
  import { findCycles } from '../domain/graph/cycles.js';
3
4
  import { loadConfig } from '../infrastructure/config.js';
4
5
  import { debug } from '../infrastructure/logger.js';
@@ -144,9 +145,10 @@ function evaluateFunctionRules(db, rules, opts, violations, ruleResults) {
144
145
  let where = "WHERE n.kind IN ('function','method')";
145
146
  const params = [];
146
147
  if (opts.noTests) where += NO_TEST_SQL;
147
- if (opts.file) {
148
- where += ' AND n.file LIKE ?';
149
- params.push(`%${opts.file}%`);
148
+ {
149
+ const fc = buildFileConditionSQL(opts.file, 'n.file');
150
+ where += fc.sql;
151
+ params.push(...fc.params);
150
152
  }
151
153
  if (opts.kind) {
152
154
  where += ' AND n.kind = ?';
@@ -221,9 +223,10 @@ function evaluateFileRules(db, rules, opts, violations, ruleResults) {
221
223
  let where = "WHERE n.kind = 'file'";
222
224
  const params = [];
223
225
  if (opts.noTests) where += NO_TEST_SQL;
224
- if (opts.file) {
225
- where += ' AND n.file LIKE ?';
226
- params.push(`%${opts.file}%`);
226
+ {
227
+ const fc = buildFileConditionSQL(opts.file, 'n.file');
228
+ where += fc.sql;
229
+ params.push(...fc.params);
227
230
  }
228
231
 
229
232
  let rows;
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { findDbPath, openReadonlyOrFail } from '../db/index.js';
4
+ import { normalizeFileFilter } from '../db/query-builder.js';
4
5
  import { isTestFile } from '../infrastructure/test-filter.js';
5
6
 
6
7
  // ─── CODEOWNERS Parsing ──────────────────────────────────────────────
@@ -192,9 +193,9 @@ export function ownersData(customDbPath, opts = {}) {
192
193
  .map((r) => r.file);
193
194
 
194
195
  if (opts.noTests) allFiles = allFiles.filter((f) => !isTestFile(f));
195
- if (opts.file) {
196
- const filter = opts.file;
197
- allFiles = allFiles.filter((f) => f.includes(filter));
196
+ const fileFilters = normalizeFileFilter(opts.file);
197
+ if (fileFilters.length > 0) {
198
+ allFiles = allFiles.filter((f) => fileFilters.some((filter) => f.includes(filter)));
198
199
  }
199
200
 
200
201
  // Map files to owners