@reinteractive/rails-insight 1.0.1
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.
- package/LICENSE +15 -0
- package/README.md +210 -0
- package/bin/railsinsight.js +128 -0
- package/package.json +62 -0
- package/src/core/blast-radius.js +496 -0
- package/src/core/constants.js +39 -0
- package/src/core/context-loader.js +227 -0
- package/src/core/drift-detector.js +168 -0
- package/src/core/formatter.js +197 -0
- package/src/core/graph.js +510 -0
- package/src/core/indexer.js +595 -0
- package/src/core/patterns/api.js +27 -0
- package/src/core/patterns/auth.js +25 -0
- package/src/core/patterns/authorization.js +24 -0
- package/src/core/patterns/caching.js +19 -0
- package/src/core/patterns/component.js +18 -0
- package/src/core/patterns/config.js +15 -0
- package/src/core/patterns/controller.js +42 -0
- package/src/core/patterns/email.js +20 -0
- package/src/core/patterns/factory.js +31 -0
- package/src/core/patterns/gemfile.js +9 -0
- package/src/core/patterns/helper.js +10 -0
- package/src/core/patterns/job.js +12 -0
- package/src/core/patterns/model.js +123 -0
- package/src/core/patterns/realtime.js +17 -0
- package/src/core/patterns/route.js +27 -0
- package/src/core/patterns/schema.js +25 -0
- package/src/core/patterns/stimulus.js +13 -0
- package/src/core/patterns/storage.js +16 -0
- package/src/core/patterns/uploader.js +16 -0
- package/src/core/patterns/view.js +20 -0
- package/src/core/patterns/worker.js +12 -0
- package/src/core/patterns.js +27 -0
- package/src/core/scanner.js +394 -0
- package/src/core/version-detector.js +295 -0
- package/src/extractors/api.js +284 -0
- package/src/extractors/auth.js +853 -0
- package/src/extractors/authorization.js +785 -0
- package/src/extractors/caching.js +84 -0
- package/src/extractors/component.js +221 -0
- package/src/extractors/config.js +81 -0
- package/src/extractors/controller.js +273 -0
- package/src/extractors/coverage-snapshot.js +296 -0
- package/src/extractors/email.js +123 -0
- package/src/extractors/factory-registry.js +225 -0
- package/src/extractors/gemfile.js +440 -0
- package/src/extractors/helper.js +55 -0
- package/src/extractors/jobs.js +122 -0
- package/src/extractors/model.js +506 -0
- package/src/extractors/realtime.js +102 -0
- package/src/extractors/routes.js +251 -0
- package/src/extractors/schema.js +178 -0
- package/src/extractors/stimulus.js +149 -0
- package/src/extractors/storage.js +100 -0
- package/src/extractors/test-conventions.js +340 -0
- package/src/extractors/tier2.js +417 -0
- package/src/extractors/tier3.js +84 -0
- package/src/extractors/uploader.js +138 -0
- package/src/extractors/views.js +131 -0
- package/src/extractors/worker.js +62 -0
- package/src/git/diff-parser.js +132 -0
- package/src/providers/interface.js +12 -0
- package/src/providers/local-fs.js +318 -0
- package/src/server.js +71 -0
- package/src/tools/blast-radius-tools.js +129 -0
- package/src/tools/free-tools.js +44 -0
- package/src/tools/handlers/get-controller.js +93 -0
- package/src/tools/handlers/get-coverage-gaps.js +100 -0
- package/src/tools/handlers/get-deep-analysis.js +294 -0
- package/src/tools/handlers/get-domain-clusters.js +113 -0
- package/src/tools/handlers/get-factory-registry.js +43 -0
- package/src/tools/handlers/get-full-index.js +28 -0
- package/src/tools/handlers/get-model.js +108 -0
- package/src/tools/handlers/get-overview.js +153 -0
- package/src/tools/handlers/get-routes.js +18 -0
- package/src/tools/handlers/get-schema.js +40 -0
- package/src/tools/handlers/get-subgraph.js +82 -0
- package/src/tools/handlers/get-test-conventions.js +18 -0
- package/src/tools/handlers/get-well-tested-examples.js +51 -0
- package/src/tools/handlers/helpers.js +115 -0
- package/src/tools/handlers/index-project.js +36 -0
- package/src/tools/handlers/search-patterns.js +104 -0
- package/src/tools/index.js +34 -0
- package/src/tools/pro-tools.js +13 -0
- package/src/utils/file-reader.js +20 -0
- package/src/utils/inflector.js +223 -0
- package/src/utils/ruby-parser.js +115 -0
- package/src/utils/spec-style-detector.js +26 -0
- package/src/utils/token-counter.js +46 -0
- package/src/utils/yaml-parser.js +135 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Conventions Extractor
|
|
3
|
+
* Analyses existing spec files to detect testing patterns, styles,
|
|
4
|
+
* and conventions used by the project.
|
|
5
|
+
*
|
|
6
|
+
* @module test-conventions
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { detectSpecStyle } from '../utils/spec-style-detector.js'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Extract test conventions from existing spec files.
|
|
13
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
14
|
+
* @param {Array<{path: string, category: number, categoryName: string, specCategory?: string}>} entries
|
|
15
|
+
* @param {{gems?: object}} gemInfo
|
|
16
|
+
* @returns {object}
|
|
17
|
+
*/
|
|
18
|
+
export function extractTestConventions(provider, entries, gemInfo = {}) {
|
|
19
|
+
const gems = gemInfo.gems || {}
|
|
20
|
+
|
|
21
|
+
const result = {
|
|
22
|
+
// Spec file style
|
|
23
|
+
spec_style: detectSpecStyle(entries),
|
|
24
|
+
|
|
25
|
+
// Let style preference
|
|
26
|
+
let_style: null,
|
|
27
|
+
let_count: 0,
|
|
28
|
+
let_bang_count: 0,
|
|
29
|
+
|
|
30
|
+
// Subject usage
|
|
31
|
+
subject_usage: false,
|
|
32
|
+
subject_count: 0,
|
|
33
|
+
|
|
34
|
+
// described_class usage
|
|
35
|
+
described_class_usage: false,
|
|
36
|
+
|
|
37
|
+
// Shared examples
|
|
38
|
+
shared_examples: [],
|
|
39
|
+
shared_examples_count: 0,
|
|
40
|
+
|
|
41
|
+
// Shared contexts
|
|
42
|
+
shared_contexts: [],
|
|
43
|
+
shared_contexts_count: 0,
|
|
44
|
+
|
|
45
|
+
// Custom matchers
|
|
46
|
+
custom_matchers: [],
|
|
47
|
+
|
|
48
|
+
// Authentication helper
|
|
49
|
+
auth_helper: detectAuthHelper(provider, entries, gems),
|
|
50
|
+
|
|
51
|
+
// Database strategy
|
|
52
|
+
database_strategy: detectDatabaseStrategy(provider, gems),
|
|
53
|
+
|
|
54
|
+
// Factory tool
|
|
55
|
+
factory_tool:
|
|
56
|
+
gems.factory_bot_rails || gems.factory_bot
|
|
57
|
+
? 'factory_bot'
|
|
58
|
+
: gems.fabrication
|
|
59
|
+
? 'fabrication'
|
|
60
|
+
: null,
|
|
61
|
+
|
|
62
|
+
// Spec file counts by category
|
|
63
|
+
spec_counts: {},
|
|
64
|
+
|
|
65
|
+
// Well-tested files (candidates for pattern reference)
|
|
66
|
+
pattern_reference_files: [],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Scan spec files for convention patterns
|
|
70
|
+
const specEntries = entries.filter(
|
|
71
|
+
(e) => e.categoryName === 'testing' && e.path.endsWith('_spec.rb'),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
// Count spec files by specCategory
|
|
75
|
+
for (const entry of specEntries) {
|
|
76
|
+
const cat = entry.specCategory || 'other'
|
|
77
|
+
result.spec_counts[cat] = (result.spec_counts[cat] || 0) + 1
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sample up to 20 spec files to detect conventions (avoid reading hundreds)
|
|
81
|
+
const sampleSize = Math.min(specEntries.length, 20)
|
|
82
|
+
const sampledEntries = specEntries.slice(0, sampleSize)
|
|
83
|
+
|
|
84
|
+
for (const entry of sampledEntries) {
|
|
85
|
+
const content = provider.readFile(entry.path)
|
|
86
|
+
if (!content) continue
|
|
87
|
+
|
|
88
|
+
// Let style detection
|
|
89
|
+
const letMatches = (content.match(/^\s*let\s*\(/gm) || []).length
|
|
90
|
+
const letBangMatches = (content.match(/^\s*let!\s*\(/gm) || []).length
|
|
91
|
+
result.let_count += letMatches
|
|
92
|
+
result.let_bang_count += letBangMatches
|
|
93
|
+
|
|
94
|
+
// Subject usage
|
|
95
|
+
if (/^\s*subject\s*[\s{(]/m.test(content)) {
|
|
96
|
+
result.subject_usage = true
|
|
97
|
+
result.subject_count++
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// described_class usage
|
|
101
|
+
if (/described_class/.test(content)) {
|
|
102
|
+
result.described_class_usage = true
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Determine let style
|
|
107
|
+
if (result.let_count > 0 || result.let_bang_count > 0) {
|
|
108
|
+
const ratio =
|
|
109
|
+
result.let_bang_count / (result.let_count + result.let_bang_count)
|
|
110
|
+
if (ratio > 0.7) result.let_style = 'eager'
|
|
111
|
+
else if (ratio < 0.3) result.let_style = 'lazy'
|
|
112
|
+
else result.let_style = 'mixed'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Scan spec/support/ for shared examples, shared contexts, and custom matchers
|
|
116
|
+
const supportEntries = entries.filter(
|
|
117
|
+
(e) => e.path.startsWith('spec/support/') && e.path.endsWith('.rb'),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
for (const entry of supportEntries) {
|
|
121
|
+
const content = provider.readFile(entry.path)
|
|
122
|
+
if (!content) continue
|
|
123
|
+
|
|
124
|
+
// Shared examples
|
|
125
|
+
const sharedExRe =
|
|
126
|
+
/(?:shared_examples_for|shared_examples|RSpec\.shared_examples)\s+['"]([^'"]+)['"]/g
|
|
127
|
+
let m
|
|
128
|
+
while ((m = sharedExRe.exec(content))) {
|
|
129
|
+
result.shared_examples.push(m[1])
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Shared contexts
|
|
133
|
+
const sharedCtxRe =
|
|
134
|
+
/(?:shared_context|RSpec\.shared_context)\s+['"]([^'"]+)['"]/g
|
|
135
|
+
while ((m = sharedCtxRe.exec(content))) {
|
|
136
|
+
result.shared_contexts.push(m[1])
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Custom matchers
|
|
140
|
+
const matcherRe = /RSpec::Matchers\.define\s+:(\w+)/g
|
|
141
|
+
while ((m = matcherRe.exec(content))) {
|
|
142
|
+
result.custom_matchers.push(m[1])
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Also check for define_negated_matcher
|
|
146
|
+
const negatedRe = /define_negated_matcher\s+:(\w+)/g
|
|
147
|
+
while ((m = negatedRe.exec(content))) {
|
|
148
|
+
result.custom_matchers.push(m[1])
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Also check spec/shared_examples/ and spec/shared_contexts/ directories
|
|
153
|
+
const sharedExampleEntries = entries.filter(
|
|
154
|
+
(e) => e.path.startsWith('spec/shared_examples/') && e.path.endsWith('.rb'),
|
|
155
|
+
)
|
|
156
|
+
for (const entry of sharedExampleEntries) {
|
|
157
|
+
const content = provider.readFile(entry.path)
|
|
158
|
+
if (!content) continue
|
|
159
|
+
const re = /(?:shared_examples_for|shared_examples)\s+['"]([^'"]+)['"]/g
|
|
160
|
+
let m
|
|
161
|
+
while ((m = re.exec(content))) {
|
|
162
|
+
result.shared_examples.push(m[1])
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result.shared_examples_count = result.shared_examples.length
|
|
167
|
+
result.shared_contexts_count = result.shared_contexts.length
|
|
168
|
+
|
|
169
|
+
// Find well-tested files as pattern references
|
|
170
|
+
result.pattern_reference_files = findPatternReferences(provider, specEntries)
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Detect authentication test helper.
|
|
177
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
178
|
+
* @param {Array<{path: string}>} entries
|
|
179
|
+
* @param {object} gems
|
|
180
|
+
* @returns {{strategy: string|null, helper_method: string|null, helper_file: string|null, setup_location: string|null}}
|
|
181
|
+
*/
|
|
182
|
+
function detectAuthHelper(provider, entries, gems) {
|
|
183
|
+
const result = {
|
|
184
|
+
strategy: null,
|
|
185
|
+
helper_method: null,
|
|
186
|
+
helper_file: null,
|
|
187
|
+
setup_location: null,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check rails_helper.rb for Devise test helpers
|
|
191
|
+
const railsHelper = provider.readFile('spec/rails_helper.rb')
|
|
192
|
+
if (railsHelper) {
|
|
193
|
+
if (/Devise::Test::IntegrationHelpers/.test(railsHelper)) {
|
|
194
|
+
result.strategy = 'devise'
|
|
195
|
+
result.helper_method = 'sign_in'
|
|
196
|
+
result.helper_file = 'spec/rails_helper.rb'
|
|
197
|
+
result.setup_location = 'spec/rails_helper.rb'
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
if (/Devise::Test::ControllerHelpers/.test(railsHelper)) {
|
|
201
|
+
result.strategy = 'devise_controller'
|
|
202
|
+
result.helper_method = 'sign_in'
|
|
203
|
+
result.helper_file = 'spec/rails_helper.rb'
|
|
204
|
+
result.setup_location = 'spec/rails_helper.rb'
|
|
205
|
+
return result
|
|
206
|
+
}
|
|
207
|
+
if (/Warden::Test::Helpers/.test(railsHelper)) {
|
|
208
|
+
result.strategy = 'warden'
|
|
209
|
+
result.helper_method = 'login_as'
|
|
210
|
+
result.helper_file = 'spec/rails_helper.rb'
|
|
211
|
+
result.setup_location = 'spec/rails_helper.rb'
|
|
212
|
+
return result
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check spec/support/ for custom auth helpers
|
|
217
|
+
const supportEntries = entries.filter(
|
|
218
|
+
(e) =>
|
|
219
|
+
e.path.startsWith('spec/support/') &&
|
|
220
|
+
e.path.endsWith('.rb') &&
|
|
221
|
+
/auth/i.test(e.path),
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
for (const entry of supportEntries) {
|
|
225
|
+
const content = provider.readFile(entry.path)
|
|
226
|
+
if (!content) continue
|
|
227
|
+
|
|
228
|
+
// Look for sign_in method definition
|
|
229
|
+
const signInMatch = content.match(
|
|
230
|
+
/def\s+(sign_in|log_in|login|authenticate)/,
|
|
231
|
+
)
|
|
232
|
+
if (signInMatch) {
|
|
233
|
+
result.strategy = 'custom'
|
|
234
|
+
result.helper_method = signInMatch[1]
|
|
235
|
+
result.helper_file = entry.path
|
|
236
|
+
result.setup_location = entry.path
|
|
237
|
+
return result
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Check for JWT/token auth patterns in support files
|
|
242
|
+
for (const entry of supportEntries) {
|
|
243
|
+
const content = provider.readFile(entry.path)
|
|
244
|
+
if (!content) continue
|
|
245
|
+
|
|
246
|
+
if (/auth.*header|bearer|jwt|token/i.test(content)) {
|
|
247
|
+
result.strategy = 'token'
|
|
248
|
+
result.helper_method = null
|
|
249
|
+
result.helper_file = entry.path
|
|
250
|
+
result.setup_location = entry.path
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Detect database cleaning/transaction strategy.
|
|
260
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
261
|
+
* @param {object} gems
|
|
262
|
+
* @returns {{strategy: string|null, config_file: string|null}}
|
|
263
|
+
*/
|
|
264
|
+
function detectDatabaseStrategy(provider, gems) {
|
|
265
|
+
const result = {
|
|
266
|
+
strategy: null,
|
|
267
|
+
config_file: null,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check rails_helper for use_transactional_fixtures
|
|
271
|
+
const railsHelper = provider.readFile('spec/rails_helper.rb') || ''
|
|
272
|
+
if (/use_transactional_fixtures\s*=\s*true/.test(railsHelper)) {
|
|
273
|
+
result.strategy = 'transactional_fixtures'
|
|
274
|
+
result.config_file = 'spec/rails_helper.rb'
|
|
275
|
+
return result
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check for database_cleaner
|
|
279
|
+
if (gems.database_cleaner || gems['database_cleaner-active_record']) {
|
|
280
|
+
result.strategy = 'database_cleaner'
|
|
281
|
+
|
|
282
|
+
// Detect strategy type
|
|
283
|
+
const supportFiles = [
|
|
284
|
+
'spec/support/database_cleaner.rb',
|
|
285
|
+
'spec/support/database_cleaner_config.rb',
|
|
286
|
+
]
|
|
287
|
+
for (const path of supportFiles) {
|
|
288
|
+
const content = provider.readFile(path)
|
|
289
|
+
if (content) {
|
|
290
|
+
result.config_file = path
|
|
291
|
+
if (/strategy\s*=\s*:truncation/.test(content)) {
|
|
292
|
+
result.strategy = 'database_cleaner:truncation'
|
|
293
|
+
} else if (/strategy\s*=\s*:transaction/.test(content)) {
|
|
294
|
+
result.strategy = 'database_cleaner:transaction'
|
|
295
|
+
}
|
|
296
|
+
break
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return result
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Find well-structured spec files as pattern references for each category.
|
|
307
|
+
* Selects the spec file with the most describe/context blocks per category.
|
|
308
|
+
* @param {import('../providers/interface.js').FileProvider} provider
|
|
309
|
+
* @param {Array<{path: string, specCategory?: string}>} specEntries
|
|
310
|
+
* @returns {Array<{path: string, category: string, describe_count: number, example_count: number}>}
|
|
311
|
+
*/
|
|
312
|
+
function findPatternReferences(provider, specEntries) {
|
|
313
|
+
const byCategory = {}
|
|
314
|
+
|
|
315
|
+
for (const entry of specEntries) {
|
|
316
|
+
const cat = entry.specCategory
|
|
317
|
+
if (!cat || cat === 'factories' || cat === 'support') continue
|
|
318
|
+
|
|
319
|
+
const content = provider.readFile(entry.path)
|
|
320
|
+
if (!content) continue
|
|
321
|
+
|
|
322
|
+
const describeCount = (content.match(/^\s*(?:describe|context)\s/gm) || [])
|
|
323
|
+
.length
|
|
324
|
+
const exampleCount = (content.match(/^\s*it\s/gm) || []).length
|
|
325
|
+
|
|
326
|
+
// Skip trivially small files
|
|
327
|
+
if (exampleCount < 3) continue
|
|
328
|
+
|
|
329
|
+
if (!byCategory[cat] || describeCount > byCategory[cat].describe_count) {
|
|
330
|
+
byCategory[cat] = {
|
|
331
|
+
path: entry.path,
|
|
332
|
+
category: cat,
|
|
333
|
+
describe_count: describeCount,
|
|
334
|
+
example_count: exampleCount,
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return Object.values(byCategory)
|
|
340
|
+
}
|