@reinteractive/rails-insight 1.0.12 → 1.0.13
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/package.json +1 -1
- package/src/core/indexer.js +6 -1
- package/src/core/patterns/model.js +1 -0
- package/src/core/patterns/route.js +1 -1
- package/src/core/version-detector.js +6 -2
- package/src/extractors/authorization.js +29 -7
- package/src/extractors/config.js +25 -0
- package/src/extractors/controller.js +56 -2
- package/src/extractors/model.js +11 -1
- package/src/extractors/test-conventions.js +11 -5
package/package.json
CHANGED
package/src/core/indexer.js
CHANGED
|
@@ -223,7 +223,11 @@ export async function buildIndex(provider, options = {}) {
|
|
|
223
223
|
extractionErrors,
|
|
224
224
|
)
|
|
225
225
|
if (model) extractions.models[className] = model
|
|
226
|
-
} else if (
|
|
226
|
+
} else if (
|
|
227
|
+
entry.categoryName === 'controllers' ||
|
|
228
|
+
(entry.categoryName === 'authentication' &&
|
|
229
|
+
entry.path.includes('_controller.rb'))
|
|
230
|
+
) {
|
|
227
231
|
const ctrl = safeExtract(
|
|
228
232
|
`controller:${entry.path}`,
|
|
229
233
|
() => extractController(provider, entry.path),
|
|
@@ -585,6 +589,7 @@ function computeStatistics(manifest, extractions, relationships) {
|
|
|
585
589
|
models: Object.values(extractions.models || {}).filter(
|
|
586
590
|
(m) => m.type !== 'concern' && !m.abstract,
|
|
587
591
|
).length,
|
|
592
|
+
models_in_manifest: (manifest.stats || {}).models || 0,
|
|
588
593
|
controllers: Object.keys(extractions.controllers || {}).length,
|
|
589
594
|
components: Object.keys(extractions.components || {}).length,
|
|
590
595
|
relationships: relationships.length,
|
|
@@ -22,6 +22,7 @@ export const MODEL_PATTERNS = {
|
|
|
22
22
|
validates: /^\s*validates?\s+:?(\w+(?:,\s*:\w+)*)(?:,\s*(.+))?$/m,
|
|
23
23
|
validate: /^\s*validate\s+:(\w+)/m,
|
|
24
24
|
validatesWithValidator: /^\s*validates_with\s+(\S+)(?:,\s*(.+))?$/m,
|
|
25
|
+
validatesOldStyle: /^\s*validates_(\w+?)(?:_of)?\s+:(\w+)(?:,\s*(.+))?$/m,
|
|
25
26
|
|
|
26
27
|
// === SCOPES ===
|
|
27
28
|
scope: /^\s*scope\s+:(\w+),\s*(?:->|lambda|proc)/m,
|
|
@@ -10,7 +10,7 @@ export const ROUTE_PATTERNS = {
|
|
|
10
10
|
constraints: /^\s*constraints\s*(?:\((.+)\))?\s*do/m,
|
|
11
11
|
httpVerb:
|
|
12
12
|
/^\s*(?:get|post|put|patch|delete)\s+['"]([^'"]+)['"](?:.*?(?:to:|=>)\s*['"]([^'"#]+)#?([^'"]*)['"'])?/m,
|
|
13
|
-
root: /^\s*root\s+(?:to
|
|
13
|
+
root: /^\s*root\s+(?:(?::to\s*=>|to:)\s*)?['"]([^'"#]+)#?([^'"]*)['"']/m,
|
|
14
14
|
mount:
|
|
15
15
|
/^\s*mount\s+(\w+(?:(?:::|\.)\w+)*)\s*(?:=>|,\s*at:)\s*['"]([^'"]+)['"]/m,
|
|
16
16
|
concern: /^\s*concern\s+:(\w+)\s+do/m,
|
|
@@ -242,9 +242,13 @@ function detectFramework(gemfile, gems, appConfig, provider) {
|
|
|
242
242
|
let cacheStore = null
|
|
243
243
|
if (hasGem('solid_cache')) cacheStore = 'solid_cache'
|
|
244
244
|
else if (hasGem('redis')) cacheStore = 'redis'
|
|
245
|
-
// Also check config
|
|
246
|
-
const
|
|
245
|
+
// Also check config — strip comment lines first to avoid false positives
|
|
246
|
+
const prodConfigRaw =
|
|
247
247
|
provider.readFile('config/environments/production.rb') || ''
|
|
248
|
+
const prodConfig = prodConfigRaw
|
|
249
|
+
.split('\n')
|
|
250
|
+
.filter((l) => !l.trim().startsWith('#'))
|
|
251
|
+
.join('\n')
|
|
248
252
|
const cacheStoreMatch = prodConfig.match(/config\.cache_store\s*=\s*:(\w+)/)
|
|
249
253
|
if (cacheStoreMatch) cacheStore = cacheStoreMatch[1]
|
|
250
254
|
|
|
@@ -576,28 +576,35 @@ export function extractAuthorization(
|
|
|
576
576
|
if (hasCanCan) {
|
|
577
577
|
if (!result.strategy) result.strategy = 'cancancan'
|
|
578
578
|
let abilityContent = provider.readFile('app/models/ability.rb')
|
|
579
|
-
|
|
579
|
+
let abilityFile = 'app/models/ability.rb'
|
|
580
|
+
// Fallback: scan model and authorization files for CanCan::Ability
|
|
580
581
|
if (!abilityContent || !AUTHORIZATION_PATTERNS.abilityClass.test(abilityContent)) {
|
|
581
|
-
const
|
|
582
|
+
const abilityEntries = entries.filter(
|
|
582
583
|
(e) =>
|
|
583
|
-
(e.category === 'model' ||
|
|
584
|
+
(e.category === 'model' ||
|
|
585
|
+
e.categoryName === 'models' ||
|
|
586
|
+
e.category === 1 ||
|
|
587
|
+
e.categoryName === 'authorization' ||
|
|
588
|
+
e.category === 9) &&
|
|
584
589
|
e.path.endsWith('.rb'),
|
|
585
590
|
)
|
|
586
|
-
for (const entry of
|
|
591
|
+
for (const entry of abilityEntries) {
|
|
587
592
|
const c = provider.readFile(entry.path)
|
|
588
593
|
if (
|
|
589
594
|
c &&
|
|
590
|
-
AUTHORIZATION_PATTERNS.abilityClass.test(c)
|
|
591
|
-
|
|
595
|
+
(AUTHORIZATION_PATTERNS.abilityClass.test(c) ||
|
|
596
|
+
AUTHORIZATION_PATTERNS.includeCanCan.test(c))
|
|
592
597
|
) {
|
|
593
598
|
abilityContent = c
|
|
599
|
+
abilityFile = entry.path
|
|
594
600
|
break
|
|
595
601
|
}
|
|
596
602
|
}
|
|
597
603
|
}
|
|
598
604
|
if (
|
|
599
605
|
abilityContent &&
|
|
600
|
-
AUTHORIZATION_PATTERNS.abilityClass.test(abilityContent)
|
|
606
|
+
(AUTHORIZATION_PATTERNS.abilityClass.test(abilityContent) ||
|
|
607
|
+
AUTHORIZATION_PATTERNS.includeCanCan.test(abilityContent))
|
|
601
608
|
) {
|
|
602
609
|
const abilities = []
|
|
603
610
|
const canRe = new RegExp(AUTHORIZATION_PATTERNS.canDef.source, 'gm')
|
|
@@ -610,6 +617,21 @@ export function extractAuthorization(
|
|
|
610
617
|
abilities.push({ type: 'cannot', definition: m[1].trim() })
|
|
611
618
|
}
|
|
612
619
|
result.abilities = abilities
|
|
620
|
+
|
|
621
|
+
// Extract roles from has_role? calls in the ability file
|
|
622
|
+
const roleRe = /has_role\?\s*\(:?['"]?(\w+)['"]?\)/g
|
|
623
|
+
const roles = new Set()
|
|
624
|
+
while ((m = roleRe.exec(abilityContent))) {
|
|
625
|
+
roles.add(m[1])
|
|
626
|
+
}
|
|
627
|
+
if (roles.size > 0) {
|
|
628
|
+
result.roles = {
|
|
629
|
+
source: 'ability_class',
|
|
630
|
+
model: 'User',
|
|
631
|
+
roles: [...roles],
|
|
632
|
+
file: abilityFile,
|
|
633
|
+
}
|
|
634
|
+
}
|
|
613
635
|
}
|
|
614
636
|
}
|
|
615
637
|
|
package/src/extractors/config.js
CHANGED
|
@@ -62,6 +62,31 @@ export function extractConfig(provider) {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
// Fallback: try config/database.yml.example
|
|
66
|
+
if (!result.database.adapter) {
|
|
67
|
+
const dbExample = provider.readFile('config/database.yml.example')
|
|
68
|
+
if (dbExample) {
|
|
69
|
+
const parsed = parseYaml(dbExample)
|
|
70
|
+
const section =
|
|
71
|
+
parsed.production || parsed.development || parsed.default || {}
|
|
72
|
+
result.database.adapter = section.adapter || null
|
|
73
|
+
if (result.database.adapter) result.database.source = 'database.yml.example'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Fallback: detect adapter from Gemfile when database.yml is absent
|
|
78
|
+
if (!result.database.adapter) {
|
|
79
|
+
const gemfile = provider.readFile('Gemfile') || ''
|
|
80
|
+
if (/gem\s+['"]mysql2['"]/.test(gemfile)) result.database.adapter = 'mysql2'
|
|
81
|
+
else if (/gem\s+['"]pg['"]/.test(gemfile))
|
|
82
|
+
result.database.adapter = 'postgresql'
|
|
83
|
+
else if (/gem\s+['"]sqlite3['"]/.test(gemfile))
|
|
84
|
+
result.database.adapter = 'sqlite3'
|
|
85
|
+
else if (/gem\s+['"]trilogy['"]/.test(gemfile))
|
|
86
|
+
result.database.adapter = 'trilogy'
|
|
87
|
+
if (result.database.adapter) result.database.source = 'gemfile'
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
// config/environments/*.rb
|
|
66
91
|
for (const env of ['production', 'development', 'test']) {
|
|
67
92
|
const content = provider.readFile(`config/environments/${env}.rb`)
|
|
@@ -40,12 +40,12 @@ export function extractController(provider, filePath) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// Filters — tag authorization guards
|
|
43
|
-
const
|
|
43
|
+
const rawFilters = []
|
|
44
44
|
const filterRe = new RegExp(CONTROLLER_PATTERNS.filterType.source, 'gm')
|
|
45
45
|
while ((m = filterRe.exec(content))) {
|
|
46
46
|
const filterMethod = m[2]
|
|
47
47
|
const isAuthzGuard = /^require_\w+!$/.test(filterMethod)
|
|
48
|
-
|
|
48
|
+
rawFilters.push({
|
|
49
49
|
type: m[1],
|
|
50
50
|
method: filterMethod,
|
|
51
51
|
...(isAuthzGuard ? { authorization_guard: true } : {}),
|
|
@@ -53,6 +53,60 @@ export function extractController(provider, filePath) {
|
|
|
53
53
|
})
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// Expand multi-method filters: `before_action :a, :b, :c` → separate entries
|
|
57
|
+
const filters = []
|
|
58
|
+
for (const filter of rawFilters) {
|
|
59
|
+
const opts = filter.options
|
|
60
|
+
if (!opts) {
|
|
61
|
+
filters.push(filter)
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Top-level comma split (ignores commas inside brackets)
|
|
66
|
+
const parts = []
|
|
67
|
+
let depth = 0
|
|
68
|
+
let current = ''
|
|
69
|
+
for (const ch of opts) {
|
|
70
|
+
if (ch === '[' || ch === '(' || ch === '{') depth++
|
|
71
|
+
else if (ch === ']' || ch === ')' || ch === '}') depth--
|
|
72
|
+
if (ch === ',' && depth === 0) {
|
|
73
|
+
parts.push(current.trim())
|
|
74
|
+
current = ''
|
|
75
|
+
} else {
|
|
76
|
+
current += ch
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (current.trim()) parts.push(current.trim())
|
|
80
|
+
|
|
81
|
+
const additionalMethods = []
|
|
82
|
+
const realOptions = []
|
|
83
|
+
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
if (/^:(\w+!?)$/.test(part)) {
|
|
86
|
+
// Bare symbol — another method, not a keyword option
|
|
87
|
+
additionalMethods.push(part.replace(/^:/, ''))
|
|
88
|
+
} else {
|
|
89
|
+
// Keyword option like `only: [:show]`, `if: :condition`
|
|
90
|
+
realOptions.push(part)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
filters.push({
|
|
95
|
+
...filter,
|
|
96
|
+
options: realOptions.length > 0 ? realOptions.join(', ') : null,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
for (const method of additionalMethods) {
|
|
100
|
+
const isAuthz = /^require_\w+!$/.test(method)
|
|
101
|
+
filters.push({
|
|
102
|
+
type: filter.type,
|
|
103
|
+
method,
|
|
104
|
+
...(isAuthz ? { authorization_guard: true } : {}),
|
|
105
|
+
options: realOptions.length > 0 ? realOptions.join(', ') : null,
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
56
110
|
// Actions (public methods before private/protected) with line ranges
|
|
57
111
|
const actions = []
|
|
58
112
|
const action_line_ranges = {}
|
package/src/extractors/model.js
CHANGED
|
@@ -127,7 +127,7 @@ export function extractModel(provider, filePath, className) {
|
|
|
127
127
|
}
|
|
128
128
|
while ((m = extendRe.exec(content))) {
|
|
129
129
|
const mod = m[1]
|
|
130
|
-
if (mod !== 'ActiveSupport::Concern'
|
|
130
|
+
if (mod !== 'ActiveSupport::Concern') {
|
|
131
131
|
extends_.push(mod)
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -179,6 +179,16 @@ export function extractModel(provider, filePath, className) {
|
|
|
179
179
|
while ((m = vwRe.exec(content))) {
|
|
180
180
|
custom_validators.push(`validates_with:${m[1]}`)
|
|
181
181
|
}
|
|
182
|
+
// Old-style validators: validates_presence_of, validates_length_of, etc.
|
|
183
|
+
const oldStyleRe = /^\s*validates_(\w+?)(?:_of)?\s+:(\w+)(?:,\s*(.+))?$/gm
|
|
184
|
+
while ((m = oldStyleRe.exec(content))) {
|
|
185
|
+
const validationType = m[1]
|
|
186
|
+
const attr = m[2]
|
|
187
|
+
validations.push({
|
|
188
|
+
attributes: [attr],
|
|
189
|
+
rules: `${validationType}: true${m[3] ? ', ' + m[3] : ''}`,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
182
192
|
|
|
183
193
|
// Scopes — names array (backward-compat) + scope_queries dict with bodies
|
|
184
194
|
const scopes = []
|
|
@@ -66,9 +66,11 @@ export function extractTestConventions(provider, entries, gemInfo = {}) {
|
|
|
66
66
|
pattern_reference_files: [],
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// Scan spec files for convention patterns
|
|
69
|
+
// Scan spec/test files for convention patterns
|
|
70
70
|
const specEntries = entries.filter(
|
|
71
|
-
(e) =>
|
|
71
|
+
(e) =>
|
|
72
|
+
e.categoryName === 'testing' &&
|
|
73
|
+
(e.path.endsWith('_spec.rb') || e.path.endsWith('_test.rb')),
|
|
72
74
|
)
|
|
73
75
|
|
|
74
76
|
// Count spec files by specCategory
|
|
@@ -319,9 +321,13 @@ function findPatternReferences(provider, specEntries) {
|
|
|
319
321
|
const content = provider.readFile(entry.path)
|
|
320
322
|
if (!content) continue
|
|
321
323
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
// Handle both RSpec and Minitest structural patterns
|
|
325
|
+
const describeCount = (
|
|
326
|
+
content.match(/^\s*(?:describe|context|class\s+\w+Test)\s/gm) || []
|
|
327
|
+
).length
|
|
328
|
+
const exampleCount = (
|
|
329
|
+
content.match(/^\s*(?:it\s|def\s+test_|test\s+['"])/gm) || []
|
|
330
|
+
).length
|
|
325
331
|
|
|
326
332
|
// Skip trivially small files
|
|
327
333
|
if (exampleCount < 3) continue
|