@sailshq/language-server 0.0.5 → 0.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.
- package/SailsParser.js +652 -0
- package/completions/actions-completion.js +36 -0
- package/completions/data-types-completion.js +39 -0
- package/completions/helper-inputs-completion.js +91 -0
- package/completions/helpers-completion.js +85 -0
- package/completions/inertia-pages-completion.js +33 -0
- package/completions/input-props-completion.js +52 -0
- package/completions/model-attribute-props-completion.js +57 -0
- package/completions/model-attributes-completion.js +195 -0
- package/completions/model-methods-completion.js +71 -0
- package/completions/models-completion.js +52 -0
- package/completions/policies-completion.js +32 -0
- package/completions/views-completion.js +35 -0
- package/go-to-definitions/go-to-action.js +26 -49
- package/go-to-definitions/go-to-helper.js +37 -45
- package/go-to-definitions/go-to-model.js +39 -0
- package/go-to-definitions/go-to-page.js +38 -0
- package/go-to-definitions/go-to-policy.js +23 -72
- package/go-to-definitions/go-to-view.js +28 -55
- package/index.js +103 -19
- package/package.json +1 -1
- package/validators/validate-action-exist.js +28 -51
- package/validators/validate-data-type.js +34 -0
- package/validators/validate-document.js +42 -4
- package/validators/validate-helper-input-exist.js +42 -0
- package/validators/validate-model-attribute-exist.js +297 -0
- package/validators/validate-model-exist.js +64 -0
- package/validators/validate-page-exist.js +42 -0
- package/validators/validate-policy-exist.js +45 -0
- package/validators/validate-required-helper-input.js +49 -0
- package/validators/validate-required-model-attribute.js +56 -0
- package/validators/validate-view-exist.js +86 -0
- package/completions/sails-completions.js +0 -63
- package/go-to-definitions/go-to-inertia-page.js +0 -53
- package/helpers/find-fn-line.js +0 -21
- package/helpers/find-project-root.js +0 -18
- package/helpers/find-sails.js +0 -12
- package/helpers/load-sails.js +0 -39
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
const acorn = require('acorn')
|
|
3
|
+
const walk = require('acorn-walk')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate if a Waterline model attribute exists when used in criteria or chainable methods.
|
|
7
|
+
* @param {TextDocument} document - The text document to validate.
|
|
8
|
+
* @param {Object} typeMap - The type map containing models and their attributes.
|
|
9
|
+
* @returns {Array} diagnostics - Array of LSP diagnostics.
|
|
10
|
+
*/
|
|
11
|
+
module.exports = function validateModelAttributeExist(document, typeMap) {
|
|
12
|
+
const diagnostics = []
|
|
13
|
+
const text = document.getText()
|
|
14
|
+
|
|
15
|
+
// Build a lowercased model map for robust case-insensitive lookup
|
|
16
|
+
const modelMap = {}
|
|
17
|
+
if (typeMap.models) {
|
|
18
|
+
for (const key of Object.keys(typeMap.models)) {
|
|
19
|
+
modelMap[key.toLowerCase()] = typeMap.models[key]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Helper function to get model by name, case-insensitive
|
|
23
|
+
function getModelByName(name) {
|
|
24
|
+
if (!name) return undefined
|
|
25
|
+
const upper = name.charAt(0).toUpperCase() + name.slice(1)
|
|
26
|
+
return typeMap.models[upper]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// AST-based: Validate Model.create({ ... }) and similar
|
|
30
|
+
try {
|
|
31
|
+
const ast = acorn.parse(text, {
|
|
32
|
+
ecmaVersion: 'latest',
|
|
33
|
+
sourceType: 'module'
|
|
34
|
+
})
|
|
35
|
+
walk.simple(ast, {
|
|
36
|
+
CallExpression(node) {
|
|
37
|
+
if (
|
|
38
|
+
node.callee &&
|
|
39
|
+
node.callee.type === 'MemberExpression' &&
|
|
40
|
+
node.arguments &&
|
|
41
|
+
node.arguments.length > 0 &&
|
|
42
|
+
node.arguments[0].type === 'ObjectExpression'
|
|
43
|
+
) {
|
|
44
|
+
const method = node.callee.property.name
|
|
45
|
+
const modelName = node.callee.object.name
|
|
46
|
+
// Only check for Waterline methods
|
|
47
|
+
if (
|
|
48
|
+
[
|
|
49
|
+
'create',
|
|
50
|
+
'createEach',
|
|
51
|
+
'count',
|
|
52
|
+
'find',
|
|
53
|
+
'findOne',
|
|
54
|
+
'update',
|
|
55
|
+
'destroy',
|
|
56
|
+
'where',
|
|
57
|
+
'findOrCreate',
|
|
58
|
+
'sum'
|
|
59
|
+
].includes(method)
|
|
60
|
+
) {
|
|
61
|
+
const model = getModelByName(modelName)
|
|
62
|
+
if (!model) return
|
|
63
|
+
for (const prop of node.arguments[0].properties) {
|
|
64
|
+
const attribute = prop.key && (prop.key.name || prop.key.value)
|
|
65
|
+
const queryOptionKeys = [
|
|
66
|
+
'where',
|
|
67
|
+
'select',
|
|
68
|
+
'omit',
|
|
69
|
+
'sort',
|
|
70
|
+
'limit',
|
|
71
|
+
'skip',
|
|
72
|
+
'page',
|
|
73
|
+
'populate',
|
|
74
|
+
'groupBy',
|
|
75
|
+
'having',
|
|
76
|
+
'sum',
|
|
77
|
+
'average',
|
|
78
|
+
'min',
|
|
79
|
+
'max',
|
|
80
|
+
'distinct',
|
|
81
|
+
'meta'
|
|
82
|
+
]
|
|
83
|
+
// --- FINAL FIX: treat 'where' exactly like 'sort', 'select', 'omit' for non-create methods ---
|
|
84
|
+
if (
|
|
85
|
+
method !== 'create' &&
|
|
86
|
+
method !== 'createEach' &&
|
|
87
|
+
queryOptionKeys.includes(attribute)
|
|
88
|
+
) {
|
|
89
|
+
if (
|
|
90
|
+
attribute === 'where' &&
|
|
91
|
+
prop.value &&
|
|
92
|
+
prop.value.type === 'ObjectExpression'
|
|
93
|
+
) {
|
|
94
|
+
for (const whereProp of prop.value.properties) {
|
|
95
|
+
if (!whereProp.key) continue
|
|
96
|
+
const whereAttr = whereProp.key.name || whereProp.key.value
|
|
97
|
+
if (
|
|
98
|
+
!model.attributes ||
|
|
99
|
+
!Object.prototype.hasOwnProperty.call(
|
|
100
|
+
model.attributes,
|
|
101
|
+
whereAttr
|
|
102
|
+
)
|
|
103
|
+
) {
|
|
104
|
+
diagnostics.push(
|
|
105
|
+
lsp.Diagnostic.create(
|
|
106
|
+
lsp.Range.create(
|
|
107
|
+
document.positionAt(whereProp.key.start),
|
|
108
|
+
document.positionAt(whereProp.key.end)
|
|
109
|
+
),
|
|
110
|
+
`'${whereAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
111
|
+
lsp.DiagnosticSeverity.Error,
|
|
112
|
+
'sails-lsp'
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
(attribute === 'select' || attribute === 'omit') &&
|
|
121
|
+
prop.value &&
|
|
122
|
+
prop.value.type === 'ArrayExpression'
|
|
123
|
+
) {
|
|
124
|
+
for (const el of prop.value.elements) {
|
|
125
|
+
if (!el) continue
|
|
126
|
+
if (el.type === 'Literal' || el.type === 'StringLiteral') {
|
|
127
|
+
const arrAttr = el.value
|
|
128
|
+
if (
|
|
129
|
+
!model.attributes ||
|
|
130
|
+
!Object.prototype.hasOwnProperty.call(
|
|
131
|
+
model.attributes,
|
|
132
|
+
arrAttr
|
|
133
|
+
)
|
|
134
|
+
) {
|
|
135
|
+
diagnostics.push(
|
|
136
|
+
lsp.Diagnostic.create(
|
|
137
|
+
lsp.Range.create(
|
|
138
|
+
document.positionAt(el.start),
|
|
139
|
+
document.positionAt(el.end)
|
|
140
|
+
),
|
|
141
|
+
`'${arrAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
142
|
+
lsp.DiagnosticSeverity.Error,
|
|
143
|
+
'sails-lsp'
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
if (attribute === 'sort' && prop.value) {
|
|
152
|
+
if (
|
|
153
|
+
prop.value.type === 'Literal' ||
|
|
154
|
+
prop.value.type === 'StringLiteral'
|
|
155
|
+
) {
|
|
156
|
+
const sortStr = prop.value.value
|
|
157
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
158
|
+
if (
|
|
159
|
+
sortAttr &&
|
|
160
|
+
(!model.attributes ||
|
|
161
|
+
!Object.prototype.hasOwnProperty.call(
|
|
162
|
+
model.attributes,
|
|
163
|
+
sortAttr
|
|
164
|
+
))
|
|
165
|
+
) {
|
|
166
|
+
diagnostics.push(
|
|
167
|
+
lsp.Diagnostic.create(
|
|
168
|
+
lsp.Range.create(
|
|
169
|
+
document.positionAt(prop.value.start),
|
|
170
|
+
document.positionAt(prop.value.end)
|
|
171
|
+
),
|
|
172
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
173
|
+
lsp.DiagnosticSeverity.Error,
|
|
174
|
+
'sails-lsp'
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
continue
|
|
179
|
+
} else if (prop.value.type === 'ArrayExpression') {
|
|
180
|
+
for (const el of prop.value.elements) {
|
|
181
|
+
if (!el) continue
|
|
182
|
+
if (el.type === 'ObjectExpression') {
|
|
183
|
+
for (const sortProp of el.properties) {
|
|
184
|
+
if (!sortProp.key) continue
|
|
185
|
+
const sortAttr =
|
|
186
|
+
sortProp.key.name || sortProp.key.value
|
|
187
|
+
if (
|
|
188
|
+
!model.attributes ||
|
|
189
|
+
!Object.prototype.hasOwnProperty.call(
|
|
190
|
+
model.attributes,
|
|
191
|
+
sortAttr
|
|
192
|
+
)
|
|
193
|
+
) {
|
|
194
|
+
diagnostics.push(
|
|
195
|
+
lsp.Diagnostic.create(
|
|
196
|
+
lsp.Range.create(
|
|
197
|
+
document.positionAt(sortProp.key.start),
|
|
198
|
+
document.positionAt(sortProp.key.end)
|
|
199
|
+
),
|
|
200
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
201
|
+
lsp.DiagnosticSeverity.Error,
|
|
202
|
+
'sails-lsp'
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (
|
|
208
|
+
el.type === 'Literal' ||
|
|
209
|
+
el.type === 'StringLiteral'
|
|
210
|
+
) {
|
|
211
|
+
const sortStr = el.value
|
|
212
|
+
const sortAttr = sortStr && sortStr.split(' ')[0]
|
|
213
|
+
if (
|
|
214
|
+
sortAttr &&
|
|
215
|
+
(!model.attributes ||
|
|
216
|
+
!Object.prototype.hasOwnProperty.call(
|
|
217
|
+
model.attributes,
|
|
218
|
+
sortAttr
|
|
219
|
+
))
|
|
220
|
+
) {
|
|
221
|
+
diagnostics.push(
|
|
222
|
+
lsp.Diagnostic.create(
|
|
223
|
+
lsp.Range.create(
|
|
224
|
+
document.positionAt(el.start),
|
|
225
|
+
document.positionAt(el.end)
|
|
226
|
+
),
|
|
227
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
228
|
+
lsp.DiagnosticSeverity.Error,
|
|
229
|
+
'sails-lsp'
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
continue
|
|
236
|
+
} else if (prop.value.type === 'ObjectExpression') {
|
|
237
|
+
for (const sortProp of prop.value.properties) {
|
|
238
|
+
if (!sortProp.key) continue
|
|
239
|
+
const sortAttr = sortProp.key.name || sortProp.key.value
|
|
240
|
+
if (
|
|
241
|
+
!model.attributes ||
|
|
242
|
+
!Object.prototype.hasOwnProperty.call(
|
|
243
|
+
model.attributes,
|
|
244
|
+
sortAttr
|
|
245
|
+
)
|
|
246
|
+
) {
|
|
247
|
+
diagnostics.push(
|
|
248
|
+
lsp.Diagnostic.create(
|
|
249
|
+
lsp.Range.create(
|
|
250
|
+
document.positionAt(sortProp.key.start),
|
|
251
|
+
document.positionAt(sortProp.key.end)
|
|
252
|
+
),
|
|
253
|
+
`'${sortAttr}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
254
|
+
lsp.DiagnosticSeverity.Error,
|
|
255
|
+
'sails-lsp'
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
continue
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// For all other query option keys, skip validation
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
// --- END ROBUST FIX ---
|
|
267
|
+
// Only validate top-level for create/createEach
|
|
268
|
+
if (
|
|
269
|
+
(method === 'create' || method === 'createEach') &&
|
|
270
|
+
(!model.attributes ||
|
|
271
|
+
!Object.prototype.hasOwnProperty.call(
|
|
272
|
+
model.attributes,
|
|
273
|
+
attribute
|
|
274
|
+
))
|
|
275
|
+
) {
|
|
276
|
+
diagnostics.push(
|
|
277
|
+
lsp.Diagnostic.create(
|
|
278
|
+
lsp.Range.create(
|
|
279
|
+
document.positionAt(prop.key.start),
|
|
280
|
+
document.positionAt(prop.key.end)
|
|
281
|
+
),
|
|
282
|
+
`'${attribute}' is not a valid attribute of model '${modelName}'. Valid attributes: ${Object.keys(model.attributes || {}).join(', ')}`,
|
|
283
|
+
lsp.DiagnosticSeverity.Error,
|
|
284
|
+
'sails-lsp'
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
} catch (err) {
|
|
294
|
+
// No regex fallback: rely solely on AST-based validation for accuracy
|
|
295
|
+
}
|
|
296
|
+
return diagnostics
|
|
297
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
/**
|
|
3
|
+
* Validate if a referenced model exists in Sails.js queries using regex.
|
|
4
|
+
* @param {TextDocument} document - The text document to validate.
|
|
5
|
+
* @param {Object} typeMap - The type map containing models.
|
|
6
|
+
* @returns {Array} diagnostics - Array of LSP diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = function validateModelExist(document, typeMap) {
|
|
9
|
+
const diagnostics = []
|
|
10
|
+
const text = document.getText()
|
|
11
|
+
// Build a lowercased model map for robust case-insensitive lookup
|
|
12
|
+
const modelMap = {}
|
|
13
|
+
if (typeMap.models) {
|
|
14
|
+
for (const key of Object.keys(typeMap.models)) {
|
|
15
|
+
modelMap[key.toLowerCase()] = typeMap.models[key]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function modelExists(name) {
|
|
19
|
+
if (!name) return false
|
|
20
|
+
return !!modelMap[name.toLowerCase()]
|
|
21
|
+
}
|
|
22
|
+
// User.find() or User.create() etc
|
|
23
|
+
const modelCallRegex =
|
|
24
|
+
/\b([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
25
|
+
let match
|
|
26
|
+
while ((match = modelCallRegex.exec(text)) !== null) {
|
|
27
|
+
const modelName = match[1]
|
|
28
|
+
if (!modelExists(modelName)) {
|
|
29
|
+
diagnostics.push(
|
|
30
|
+
lsp.Diagnostic.create(
|
|
31
|
+
lsp.Range.create(
|
|
32
|
+
document.positionAt(match.index),
|
|
33
|
+
document.positionAt(match.index + modelName.length)
|
|
34
|
+
),
|
|
35
|
+
`Model '${modelName}' not found. Make sure it exists under your api/models directory.`,
|
|
36
|
+
lsp.DiagnosticSeverity.Error,
|
|
37
|
+
'sails-lsp'
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// sails.models.user.find() or sails.models.User.find()
|
|
43
|
+
const sailsModelCallRegex =
|
|
44
|
+
/sails\.models\.([A-Za-z0-9_]+)\s*\.(?:find|findOne|create|createEach|update|destroy|count|sum|where|findOrCreate)\s*\(/g
|
|
45
|
+
while ((match = sailsModelCallRegex.exec(text)) !== null) {
|
|
46
|
+
const modelName = match[1]
|
|
47
|
+
if (!modelExists(modelName)) {
|
|
48
|
+
diagnostics.push(
|
|
49
|
+
lsp.Diagnostic.create(
|
|
50
|
+
lsp.Range.create(
|
|
51
|
+
document.positionAt(match.index + 'sails.models.'.length),
|
|
52
|
+
document.positionAt(
|
|
53
|
+
match.index + 'sails.models.'.length + modelName.length
|
|
54
|
+
)
|
|
55
|
+
),
|
|
56
|
+
`Model '${modelName}' does not exist in this Sails project.`,
|
|
57
|
+
lsp.DiagnosticSeverity.Error,
|
|
58
|
+
'sails-lsp'
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return diagnostics
|
|
64
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validatePageExist(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
|
|
6
|
+
const pages = extractPageReferences(document)
|
|
7
|
+
for (const { page, range } of pages) {
|
|
8
|
+
if (!typeMap.pages?.[page]) {
|
|
9
|
+
diagnostics.push(
|
|
10
|
+
lsp.Diagnostic.create(
|
|
11
|
+
range,
|
|
12
|
+
`Inertia page '${page}' not found. Make sure it exists under your /assets/js/pages directory.`,
|
|
13
|
+
lsp.DiagnosticSeverity.Error,
|
|
14
|
+
'sails-lsp'
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return diagnostics
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractPageReferences(document) {
|
|
23
|
+
const text = document.getText()
|
|
24
|
+
const regex = /\bpage\s*:\s*['"]([^'"]+)['"]/g
|
|
25
|
+
const pages = []
|
|
26
|
+
|
|
27
|
+
let match
|
|
28
|
+
while ((match = regex.exec(text)) !== null) {
|
|
29
|
+
const page = match[1]
|
|
30
|
+
const pageStart = match.index + match[0].indexOf(page)
|
|
31
|
+
const pageEnd = pageStart + page.length
|
|
32
|
+
|
|
33
|
+
pages.push({
|
|
34
|
+
page,
|
|
35
|
+
range: lsp.Range.create(
|
|
36
|
+
document.positionAt(pageStart),
|
|
37
|
+
document.positionAt(pageEnd)
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
return pages
|
|
42
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validatePolicyExist(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
if (!document.uri.endsWith('policies.js')) return diagnostics
|
|
6
|
+
|
|
7
|
+
const policyRefs = extractPolicyReferences(document)
|
|
8
|
+
for (const { policy, range } of policyRefs) {
|
|
9
|
+
if (!typeMap.policies?.[policy]) {
|
|
10
|
+
diagnostics.push(
|
|
11
|
+
lsp.Diagnostic.create(
|
|
12
|
+
range,
|
|
13
|
+
`Policy '${policy}' not found. Make sure it exists in api/policies.`,
|
|
14
|
+
lsp.DiagnosticSeverity.Error,
|
|
15
|
+
'sails-lsp'
|
|
16
|
+
)
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return diagnostics
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractPolicyReferences(document) {
|
|
25
|
+
const text = document.getText()
|
|
26
|
+
const policyRegex = /(?::|=)\s*(?:\[\s*)?['"]([^'"]+)['"]/g
|
|
27
|
+
const policies = []
|
|
28
|
+
|
|
29
|
+
let match
|
|
30
|
+
while ((match = policyRegex.exec(text)) !== null) {
|
|
31
|
+
const policy = match[1]
|
|
32
|
+
const policyStart = match.index + match[0].indexOf(policy)
|
|
33
|
+
const policyEnd = policyStart + policy.length
|
|
34
|
+
|
|
35
|
+
policies.push({
|
|
36
|
+
policy,
|
|
37
|
+
range: lsp.Range.create(
|
|
38
|
+
document.positionAt(policyStart),
|
|
39
|
+
document.positionAt(policyEnd)
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return policies
|
|
45
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateRequiredHelperInput(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
const text = document.getText()
|
|
6
|
+
|
|
7
|
+
// Regex to match sails.helpers.foo.bar.with({ ... })
|
|
8
|
+
// Captures: 1) helper path, 2) object literal content
|
|
9
|
+
const regex = /sails\.helpers((?:\.[a-zA-Z0-9_]+)+)\.with\s*\(\s*\{([^}]*)\}/g
|
|
10
|
+
let match
|
|
11
|
+
while ((match = regex.exec(text)) !== null) {
|
|
12
|
+
// Build helper name: e.g. .foo.bar => foo/bar
|
|
13
|
+
const segments = match[1].split('.').filter(Boolean)
|
|
14
|
+
const toKebab = (s) => s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
15
|
+
const fullHelperName = segments.map(toKebab).join('/')
|
|
16
|
+
const helperInfo = typeMap.helpers && typeMap.helpers[fullHelperName]
|
|
17
|
+
if (!helperInfo || !helperInfo.inputs) continue
|
|
18
|
+
|
|
19
|
+
// Find all property names in the object literal
|
|
20
|
+
const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
|
|
21
|
+
let propMatch
|
|
22
|
+
const providedKeys = new Set()
|
|
23
|
+
while ((propMatch = propsRegex.exec(match[2])) !== null) {
|
|
24
|
+
providedKeys.add(propMatch[1])
|
|
25
|
+
}
|
|
26
|
+
// Check for missing required inputs (support boolean or string 'required')
|
|
27
|
+
for (const [inputKey, inputDef] of Object.entries(helperInfo.inputs)) {
|
|
28
|
+
const isRequired =
|
|
29
|
+
inputDef && (inputDef.required === true || inputDef.required === 'true')
|
|
30
|
+
if (isRequired && !providedKeys.has(inputKey)) {
|
|
31
|
+
// Find the start/end of the object literal for the diagnostic range
|
|
32
|
+
const objStart = match.index + match[0].indexOf('{')
|
|
33
|
+
const objEnd = objStart + match[2].length + 1 // +1 for closing }
|
|
34
|
+
diagnostics.push(
|
|
35
|
+
lsp.Diagnostic.create(
|
|
36
|
+
lsp.Range.create(
|
|
37
|
+
document.positionAt(objStart),
|
|
38
|
+
document.positionAt(objEnd)
|
|
39
|
+
),
|
|
40
|
+
`Missing required input '${inputKey}' for helper '${fullHelperName}'.`,
|
|
41
|
+
lsp.DiagnosticSeverity.Error,
|
|
42
|
+
'sails-lsp'
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return diagnostics
|
|
49
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateRequiredModelAttribute(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
const text = document.getText()
|
|
6
|
+
const models = typeMap.models || {}
|
|
7
|
+
|
|
8
|
+
// For each model, check for .create({ ... }) and .createEach({ ... }) calls
|
|
9
|
+
for (const [modelName, modelInfo] of Object.entries(models)) {
|
|
10
|
+
if (!modelInfo || !modelInfo.attributes) continue
|
|
11
|
+
// Only validate required attributes for create and createEach
|
|
12
|
+
const methodRegex = new RegExp(
|
|
13
|
+
`${modelName}\\s*\\.\\s*(create|createEach)\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
14
|
+
'g'
|
|
15
|
+
)
|
|
16
|
+
let match
|
|
17
|
+
while ((match = methodRegex.exec(text)) !== null) {
|
|
18
|
+
// match[2] is the object literal content
|
|
19
|
+
// Find all property names in the object literal, including shorthand
|
|
20
|
+
const propsRegex = /([a-zA-Z0-9_]+)\s*:/g
|
|
21
|
+
const shorthandRegex = /([a-zA-Z0-9_]+)\s*(,|(?=\n|\}))/g
|
|
22
|
+
let propMatch
|
|
23
|
+
const providedKeys = new Set()
|
|
24
|
+
while ((propMatch = propsRegex.exec(match[2])) !== null) {
|
|
25
|
+
providedKeys.add(propMatch[1])
|
|
26
|
+
}
|
|
27
|
+
while ((propMatch = shorthandRegex.exec(match[2])) !== null) {
|
|
28
|
+
if (!providedKeys.has(propMatch[1])) {
|
|
29
|
+
providedKeys.add(propMatch[1])
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// Check for missing required attributes
|
|
33
|
+
for (const [attr, def] of Object.entries(modelInfo.attributes)) {
|
|
34
|
+
const isRequired =
|
|
35
|
+
def && (def.required === true || def.required === 'true')
|
|
36
|
+
if (isRequired && !providedKeys.has(attr)) {
|
|
37
|
+
// Find the start/end of the object literal for the diagnostic range
|
|
38
|
+
const objStart = match.index + match[0].indexOf('{')
|
|
39
|
+
const objEnd = objStart + match[2].length + 1 // +1 for closing }
|
|
40
|
+
diagnostics.push(
|
|
41
|
+
lsp.Diagnostic.create(
|
|
42
|
+
lsp.Range.create(
|
|
43
|
+
document.positionAt(objStart),
|
|
44
|
+
document.positionAt(objEnd)
|
|
45
|
+
),
|
|
46
|
+
`Missing required attribute '${attr}' in ${modelName}.${match[1]}().`,
|
|
47
|
+
lsp.DiagnosticSeverity.Error,
|
|
48
|
+
'sails-lsp'
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return diagnostics
|
|
56
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const lsp = require('vscode-languageserver/node')
|
|
2
|
+
|
|
3
|
+
module.exports = function validateViewExist(document, typeMap) {
|
|
4
|
+
const diagnostics = []
|
|
5
|
+
|
|
6
|
+
// Only check routes.js and actions (api/controllers)
|
|
7
|
+
const isRoutes = document.uri.endsWith('routes.js')
|
|
8
|
+
const isAction = document.uri.includes('/api/controllers/')
|
|
9
|
+
if (!isRoutes && !isAction) return diagnostics
|
|
10
|
+
|
|
11
|
+
// Extract { view: '...' } in routes.js
|
|
12
|
+
if (isRoutes) {
|
|
13
|
+
const views = extractViewReferences(document)
|
|
14
|
+
for (const { view, range } of views) {
|
|
15
|
+
if (!typeMap.views?.[view]) {
|
|
16
|
+
diagnostics.push(
|
|
17
|
+
lsp.Diagnostic.create(
|
|
18
|
+
range,
|
|
19
|
+
`View '${view}' not found. Make sure it exists in your /views directory.`,
|
|
20
|
+
lsp.DiagnosticSeverity.Error,
|
|
21
|
+
'sails-lsp'
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Extract viewTemplatePath: '...' in actions
|
|
29
|
+
if (isAction) {
|
|
30
|
+
const exits = extractViewTemplatePathReferences(document)
|
|
31
|
+
for (const { view, range } of exits) {
|
|
32
|
+
if (!typeMap.views?.[view]) {
|
|
33
|
+
diagnostics.push(
|
|
34
|
+
lsp.Diagnostic.create(
|
|
35
|
+
range,
|
|
36
|
+
`View '${view}' not found. Make sure it exists in your /views directory.`,
|
|
37
|
+
lsp.DiagnosticSeverity.Error,
|
|
38
|
+
'sails-lsp'
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return diagnostics
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractViewReferences(document) {
|
|
49
|
+
const text = document.getText()
|
|
50
|
+
const regex = /view\s*:\s*['"]([^'"]+)['"]/g
|
|
51
|
+
const views = []
|
|
52
|
+
let match
|
|
53
|
+
while ((match = regex.exec(text)) !== null) {
|
|
54
|
+
const view = match[1]
|
|
55
|
+
const viewStart = match.index + match[0].indexOf(view)
|
|
56
|
+
const viewEnd = viewStart + view.length
|
|
57
|
+
views.push({
|
|
58
|
+
view,
|
|
59
|
+
range: lsp.Range.create(
|
|
60
|
+
document.positionAt(viewStart),
|
|
61
|
+
document.positionAt(viewEnd)
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return views
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function extractViewTemplatePathReferences(document) {
|
|
69
|
+
const text = document.getText()
|
|
70
|
+
const regex = /viewTemplatePath\s*:\s*['"]([^'"]+)['"]/g
|
|
71
|
+
const views = []
|
|
72
|
+
let match
|
|
73
|
+
while ((match = regex.exec(text)) !== null) {
|
|
74
|
+
const view = match[1]
|
|
75
|
+
const viewStart = match.index + match[0].indexOf(view)
|
|
76
|
+
const viewEnd = viewStart + view.length
|
|
77
|
+
views.push({
|
|
78
|
+
view,
|
|
79
|
+
range: lsp.Range.create(
|
|
80
|
+
document.positionAt(viewStart),
|
|
81
|
+
document.positionAt(viewEnd)
|
|
82
|
+
)
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
return views
|
|
86
|
+
}
|