@sailshq/language-server 0.1.0 → 0.2.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.
@@ -13,12 +13,17 @@ module.exports = function inputPropsCompletion(document, position, typeMap) {
13
13
  const offset = document.offsetAt(position)
14
14
  const before = text.substring(0, offset)
15
15
 
16
+ const lines = before.split('\n')
17
+ const lastLine = lines[lines.length - 1]
18
+
19
+ // Only trigger if we're on a new line with optional whitespace (no code)
20
+ if (!/^\s*$/.test(lastLine)) return []
21
+
16
22
  // Check we're inside the inputs: { ... } section
17
23
  const insideInputs = /inputs\s*:\s*{[\s\S]*$/.test(before)
18
24
  if (!insideInputs) return []
19
25
 
20
- // Walk backward to see if we're inside an input property definition
21
- const lines = before.split('\n')
26
+ // Walk backward to see if we're inside an input block
22
27
  const reversed = lines.slice().reverse()
23
28
  let insideInputBlock = false
24
29
 
@@ -28,29 +33,20 @@ module.exports = function inputPropsCompletion(document, position, typeMap) {
28
33
  insideInputBlock = true
29
34
  break
30
35
  }
31
- if (/^\}/.test(trimmed)) {
32
- break // exited a block
33
- }
36
+ if (/^\}/.test(trimmed)) break
34
37
  }
35
38
 
36
39
  if (!insideInputBlock) return []
37
40
 
38
- // Extract current typing prefix
39
- const lastLine = lines[lines.length - 1]
40
- const prefixMatch = lastLine.match(/([a-zA-Z0-9_]*)$/)
41
- const prefix = prefixMatch ? prefixMatch[1] : ''
42
-
43
- return typeMap.inputProps
44
- .filter(({ label }) => label.startsWith(prefix))
45
- .map(({ label, detail }) => ({
46
- label,
47
- kind:
48
- label === 'custom'
49
- ? lsp.CompletionItemKind.Method
50
- : lsp.CompletionItemKind.Field,
51
- detail,
52
- documentation: detail,
53
- insertText: `${label}: `,
54
- insertTextFormat: lsp.InsertTextFormat.PlainText
55
- }))
41
+ return typeMap.inputProps.map(({ label, detail }) => ({
42
+ label,
43
+ kind:
44
+ label === 'custom'
45
+ ? lsp.CompletionItemKind.Method
46
+ : lsp.CompletionItemKind.Field,
47
+ detail,
48
+ documentation: detail,
49
+ insertText: `${label}: `,
50
+ insertTextFormat: lsp.InsertTextFormat.PlainText
51
+ }))
56
52
  }
@@ -6,48 +6,52 @@ module.exports = function modelAttributePropsCompletion(
6
6
  typeMap
7
7
  ) {
8
8
  const filePath = document.uri
9
+ if (!filePath.includes('/api/models/')) return []
9
10
 
10
- const isTargetFile = filePath.includes('/api/models/')
11
- if (!isTargetFile) return []
12
11
  const text = document.getText()
13
12
  const offset = document.offsetAt(position)
14
13
  const before = text.substring(0, offset)
14
+ const lines = before.split('\n')
15
+ const lastLine = lines[lines.length - 1]
16
+
17
+ // Only trigger on an empty or whitespace-only line
18
+ if (!/^\s*$/.test(lastLine)) return []
15
19
 
16
20
  // Confirm we're inside the attributes section
17
- const insideAttributes = /attributes\s*:\s*{[\s\S]*$/.test(before)
21
+ const insideAttributes = /attributes\s*:\s*{([\s\S]*)$/.exec(before)
18
22
  if (!insideAttributes) return []
19
23
 
20
- // Try to match "someProperty: {" above the current line
21
- const lines = before.split('\n')
22
- const reversed = lines.slice().reverse()
23
- let insidePropertyBlock = false
24
-
25
- for (const line of reversed) {
26
- const trimmed = line.trim()
27
- if (/^[a-zA-Z0-9_]+\s*:\s*{\s*$/.test(trimmed)) {
28
- insidePropertyBlock = true
29
- break
30
- }
31
- if (/^\}/.test(trimmed)) {
32
- break // exited a block without entering a new one
24
+ // Use a stack to track braces and find if we're inside a property block
25
+ let braceStack = []
26
+ let insideProperty = false
27
+ for (let i = lines.length - 1; i >= 0; i--) {
28
+ const line = lines[i]
29
+ for (let j = line.length - 1; j >= 0; j--) {
30
+ if (line[j] === '}') braceStack.push('}')
31
+ if (line[j] === '{') {
32
+ if (braceStack.length > 0) {
33
+ braceStack.pop()
34
+ } else {
35
+ const propMatch = lines[i]
36
+ .slice(0, j + 1)
37
+ .match(/([a-zA-Z0-9_]+)\s*:\s*{$/)
38
+ if (propMatch) insideProperty = true
39
+ break
40
+ }
41
+ }
33
42
  }
43
+ if (insideProperty) break
44
+ if (/^\s*attributes\s*:\s*{/.test(line)) break
34
45
  }
35
46
 
36
- if (!insidePropertyBlock) return []
37
-
38
- // Optional: match current prefix
39
- const lastLine = lines[lines.length - 1]
40
- const prefixMatch = lastLine.match(/([a-zA-Z0-9_]*)$/)
41
- const prefix = prefixMatch ? prefixMatch[1] : ''
47
+ if (!insideProperty) return []
42
48
 
43
- return typeMap.modelAttributeProps
44
- .filter(({ label }) => label.startsWith(prefix))
45
- .map(({ label, detail }) => ({
46
- label,
47
- kind: lsp.CompletionItemKind.Field,
48
- detail,
49
- documentation: detail,
50
- insertText: `${label}: `,
51
- insertTextFormat: lsp.InsertTextFormat.PlainText
52
- }))
49
+ return typeMap.modelAttributeProps.map(({ label, detail }) => ({
50
+ label,
51
+ kind: lsp.CompletionItemKind.Field,
52
+ detail,
53
+ documentation: detail,
54
+ insertText: `${label}: `,
55
+ insertTextFormat: lsp.InsertTextFormat.PlainText
56
+ }))
53
57
  }
@@ -29,12 +29,71 @@ module.exports = function modelAttributesCompletion(
29
29
  )
30
30
  const sortStringMatch = before.match(/sort\s*:\s*['"]([a-zA-Z0-9_]*)?$/)
31
31
  const sortArrayStringMatch = before.match(
32
- /sort\s*:\s*\[\s*[^{\]]*['"]([a-zA-Z0-9_]*)?$/
32
+ /sort\s*:\s*\[\s*[^\{\]]*['"]([a-zA-Z0-9_]*)?$/
33
33
  )
34
34
  const sortArrayObjectMatch = before.match(
35
35
  /sort\s*:\s*\[\s*\{\s*([a-zA-Z0-9_]*)?$/
36
36
  )
37
37
 
38
+ // Detect if we are inside a .select(['']), .omit(['']), .sort(['']), etc. chainable method call context
39
+ // This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
40
+ const chainableMethodCallRegex =
41
+ /\.(select|omit|sort)\s*\(\s*([\[\{]?[^\)]*)$/
42
+ const isInChainableMethodCall = chainableMethodCallRegex.test(before)
43
+
44
+ // Detect if we are inside a .select([]), .omit([]), .sort([]), etc. as a method call (e.g. User.find().select([]))
45
+ // This matches e.g. .select(['foo', '']) or .omit(["bar", '']) or .select('foo')
46
+ const chainableDirectCallMatch = before.match(
47
+ /\.(select|omit|sort|populate|where)\s*\(\s*\[?\s*['"]?([a-zA-Z0-9_]*)?$/
48
+ )
49
+
50
+ // Also allow completions in .where({ ... }) chainable method call context
51
+ const whereMethodCallRegex = /\.where\s*\(\s*\{[^\)]*$/
52
+ const isInWhereMethodCall = whereMethodCallRegex.test(before)
53
+
54
+ // Add: detect .where({ ... }) context for modelName/prefix inference
55
+ const whereMethodCallMatch = before.match(
56
+ /([A-Za-z_$][\w$]*)\s*\.where\s*\(\s*\{[^}]*([a-zA-Z0-9_]*)?$/
57
+ )
58
+
59
+ // Only suppress completions after a colon (:) in object literals for static methods,
60
+ // but always allow completions in .select(['']), .omit(['']), .sort(['']), .where({}), etc.
61
+ const inChainableString =
62
+ selectStringMatch ||
63
+ selectArrayMatch ||
64
+ sortStringMatch ||
65
+ sortArrayStringMatch ||
66
+ sortArrayObjectMatch ||
67
+ populateStringMatch ||
68
+ isInChainableMethodCall ||
69
+ isInWhereMethodCall ||
70
+ !!chainableDirectCallMatch
71
+
72
+ // Suppress completions after a colon only if NOT in a chainable string/array context
73
+ if (!inChainableString) {
74
+ const lines = before.split('\n')
75
+ const line = lines[lines.length - 1]
76
+ const beforeCursor = line.slice(0, position.character)
77
+ // If the last non-whitespace character before the cursor is a colon, suppress completion
78
+ // (but allow after comma, or at start of line/object)
79
+ const lastColon = beforeCursor.lastIndexOf(':')
80
+ const lastComma = beforeCursor.lastIndexOf(',')
81
+ if (lastColon > lastComma && lastColon > beforeCursor.lastIndexOf('{')) {
82
+ // Check if we are inside a string (e.g. after a colon and inside quotes)
83
+ // If so, suppress completion
84
+ const quoteBefore = beforeCursor.lastIndexOf("'")
85
+ const dquoteBefore = beforeCursor.lastIndexOf('"')
86
+ if (
87
+ (quoteBefore > lastColon && quoteBefore > lastComma) ||
88
+ (dquoteBefore > lastColon && dquoteBefore > lastComma)
89
+ ) {
90
+ return []
91
+ }
92
+ // Otherwise, suppress completion after colon
93
+ return []
94
+ }
95
+ }
96
+
38
97
  let modelName,
39
98
  prefix,
40
99
  attributes,
@@ -69,6 +128,12 @@ module.exports = function modelAttributesCompletion(
69
128
  modelName = inferModelName(before)
70
129
  prefix =
71
130
  (sortStringMatch || sortArrayStringMatch || sortArrayObjectMatch)[1] || ''
131
+ } else if (whereMethodCallMatch) {
132
+ modelName = whereMethodCallMatch[1]
133
+ prefix = whereMethodCallMatch[2] || ''
134
+ } else if (chainableDirectCallMatch) {
135
+ modelName = inferModelName(before)
136
+ prefix = chainableDirectCallMatch[2] || ''
72
137
  } else {
73
138
  return []
74
139
  }
@@ -80,6 +145,27 @@ module.exports = function modelAttributesCompletion(
80
145
  const model = foundKey ? models[foundKey] : null
81
146
  if (!model) return []
82
147
 
148
+ // Find already-used property names in the current object literal (if inside one)
149
+ let usedProps = new Set()
150
+ // Use the text before the cursor to find the nearest opening brace
151
+ const beforeCursorFull = text.slice(0, offset)
152
+ const lastOpen = beforeCursorFull.lastIndexOf('{')
153
+ const lastClose = beforeCursorFull.lastIndexOf('}')
154
+ if (lastOpen !== -1 && (lastClose === -1 || lastOpen > lastClose)) {
155
+ // Only consider properties before the cursor
156
+ const objectText = beforeCursorFull.slice(lastOpen, offset)
157
+ // Match both foo: ... and object shorthand foo,
158
+ const propRegex = /([a-zA-Z0-9_]+)\s*:/g
159
+ const shorthandRegex = /([a-zA-Z0-9_]+)\s*,/g
160
+ let m
161
+ while ((m = propRegex.exec(objectText)) !== null) {
162
+ usedProps.add(m[1])
163
+ }
164
+ while ((m = shorthandRegex.exec(objectText)) !== null) {
165
+ usedProps.add(m[1])
166
+ }
167
+ }
168
+
83
169
  if (isPopulate) {
84
170
  attributes = Object.entries(model.attributes || {})
85
171
  .filter(([, def]) => def && (def.model || def.collection))
@@ -90,13 +176,20 @@ module.exports = function modelAttributesCompletion(
90
176
 
91
177
  return attributes
92
178
  .filter((attr) => attr.startsWith(prefix))
93
- .map((attr) => ({
94
- label: attr,
95
- kind: lsp.CompletionItemKind.Field,
96
- detail: `Attribute of ${modelName}`,
97
- documentation: `${modelName}.${attr}`,
98
- sortText: attr,
99
- filterText: attr,
100
- insertText: attr
101
- }))
179
+ .filter((attr) => !usedProps.has(attr))
180
+ .map((attr) => {
181
+ const attrDef = model.attributes && model.attributes[attr]
182
+ let type = attrDef && attrDef.type ? attrDef.type : ''
183
+ let required = attrDef && attrDef.required ? 'required' : 'optional'
184
+ let detail = type ? `${type} (${required})` : required
185
+ return {
186
+ label: attr,
187
+ kind: lsp.CompletionItemKind.Field,
188
+ detail,
189
+ documentation: `${modelName}.${attr}`,
190
+ sortText: attr,
191
+ filterText: attr,
192
+ insertText: attr
193
+ }
194
+ })
102
195
  }
@@ -48,14 +48,24 @@ module.exports = function modelMethodsCompletion(document, position, typeMap) {
48
48
 
49
49
  return methods
50
50
  .filter((method) => method.name.startsWith(prefix))
51
- .map((method) => ({
52
- label: method.name,
53
- kind: lsp.CompletionItemKind.Method,
54
- detail: method.description,
55
- documentation: `${modelName}.${method.name}()`,
56
- sortText: method.name,
57
- filterText: method.name,
58
- insertText: method.name + '($0)',
59
- insertTextFormat: lsp.InsertTextFormat.Snippet
60
- }))
51
+ .map((method) => {
52
+ let insertText = method.name + '($0)'
53
+ // For chainable .select or .omit, insert ([''])
54
+ if (
55
+ chainableCallMatch &&
56
+ (method.name === 'select' || method.name === 'omit')
57
+ ) {
58
+ insertText = method.name + '([$0])'
59
+ }
60
+ return {
61
+ label: method.name,
62
+ kind: lsp.CompletionItemKind.Method,
63
+ detail: method.description,
64
+ documentation: `${modelName}.${method.name}()`,
65
+ sortText: method.name,
66
+ filterText: method.name,
67
+ insertText,
68
+ insertTextFormat: lsp.InsertTextFormat.Snippet
69
+ }
70
+ })
61
71
  }
@@ -8,40 +8,45 @@ module.exports = async function goToHelper(document, position, typeMap) {
8
8
  const text = document.getText()
9
9
  const offset = document.offsetAt(position)
10
10
 
11
- // Match sails.helpers.foo or sails.helpers.bar.baz
12
- const regex =
13
- /\bsails\.helpers(?:\.(?<group>[a-zA-Z0-9_]+))?\.(?<helper>[a-zA-Z0-9_]+)(?![\w.])/g
11
+ // Regex to match sails.helpers.foo.bar (even if chained, e.g. .with, .with(), .with({}), etc.)
12
+ // This is similar to go-to-model: match the helper path, then allow any chain after
13
+ const regex = /\bsails\.helpers((?:\.[A-Za-z0-9_]+)+)/g
14
14
 
15
15
  let match
16
-
17
16
  while ((match = regex.exec(text)) !== null) {
18
- const { group, helper } = match.groups
19
-
20
- const kebabGroup = group ? toKebab(group) : null
21
- const kebabHelper = toKebab(helper)
22
- const fullHelperName = kebabGroup
23
- ? `${kebabGroup}/${kebabHelper}`
24
- : kebabHelper
25
-
26
- // Compute accurate range for just the helper name
27
- const helperStart = match.index + match[0].lastIndexOf(helper)
28
- const helperEnd = helperStart + helper.length
29
-
30
- if (offset >= helperStart && offset <= helperEnd) {
31
- const helperInfo = typeMap.helpers?.[fullHelperName]
32
- if (helperInfo && helperInfo.path) {
33
- const uri = `file://${helperInfo.path}`
34
- return lsp.LocationLink.create(
35
- uri,
36
- lsp.Range.create(helperInfo.fnLine - 1, 0, helperInfo.fnLine - 1, 0),
37
- lsp.Range.create(helperInfo.fnLine - 1, 0, helperInfo.fnLine - 1, 0),
38
- lsp.Range.create(
39
- document.positionAt(helperStart),
40
- document.positionAt(helperEnd)
41
- )
17
+ const segments = match[1].slice(1).split('.') // drop the leading dot
18
+ if (!segments.length) continue
19
+
20
+ // Only use the last segment before .with as the helper name
21
+ let cleanSegments = segments
22
+ if (segments[segments.length - 1] === 'with') {
23
+ cleanSegments = segments.slice(0, -1)
24
+ }
25
+ const fullHelperName = cleanSegments.map(toKebab).join('/')
26
+ const lastSeg = cleanSegments[cleanSegments.length - 1]
27
+ const helperStart = match.index + match[0].lastIndexOf(lastSeg)
28
+ const helperEnd = helperStart + lastSeg.length
29
+
30
+ // Allow go-to if the cursor is anywhere inside the helper name
31
+ if (offset < helperStart || offset > helperEnd) {
32
+ continue
33
+ }
34
+
35
+ // Now look up in your typeMap
36
+ const info = typeMap.helpers?.[fullHelperName]
37
+ if (info?.path) {
38
+ const uri = `file://${info.path}`
39
+ return lsp.LocationLink.create(
40
+ uri,
41
+ lsp.Range.create(info.fnLine - 1, 0, info.fnLine - 1, 0),
42
+ lsp.Range.create(info.fnLine - 1, 0, info.fnLine - 1, 0),
43
+ lsp.Range.create(
44
+ document.positionAt(helperStart),
45
+ document.positionAt(helperEnd)
42
46
  )
43
- }
47
+ )
44
48
  }
45
49
  }
50
+
46
51
  return null
47
52
  }
package/index.js CHANGED
@@ -24,6 +24,9 @@ const policiesCompletion = require('./completions/policies-completion')
24
24
  const viewsCompletion = require('./completions/views-completion')
25
25
  const modelMethodsCompletion = require('./completions/model-methods-completion')
26
26
  const modelAttributesCompletion = require('./completions/model-attributes-completion')
27
+ const helperInputsCompletion = require('./completions/helper-inputs-completion')
28
+ const helpersCompletion = require('./completions/helpers-completion')
29
+
27
30
  const connection = lsp.createConnection(lsp.ProposedFeatures.all)
28
31
  const documents = new lsp.TextDocuments(TextDocument)
29
32
 
@@ -116,7 +119,9 @@ connection.onCompletion(async (params) => {
116
119
  policyCompletion,
117
120
  viewCompletion,
118
121
  modelMethodCompletion,
119
- modelAttributeCompletion
122
+ modelAttributeCompletion,
123
+ helperCompletion,
124
+ helperInputCompletion
120
125
  ] = await Promise.all([
121
126
  actionsCompletion(document, params.position, typeMap),
122
127
  dataTypesCompletion(document, params.position, typeMap),
@@ -127,7 +132,9 @@ connection.onCompletion(async (params) => {
127
132
  policiesCompletion(document, params.position, typeMap),
128
133
  viewsCompletion(document, params.position, typeMap),
129
134
  modelMethodsCompletion(document, params.position, typeMap),
130
- modelAttributesCompletion(document, params.position, typeMap)
135
+ modelAttributesCompletion(document, params.position, typeMap),
136
+ helpersCompletion(document, params.position, typeMap),
137
+ helperInputsCompletion(document, params.position, typeMap)
131
138
  ])
132
139
 
133
140
  const completions = [
@@ -140,7 +147,9 @@ connection.onCompletion(async (params) => {
140
147
  ...policyCompletion,
141
148
  ...viewCompletion,
142
149
  ...modelMethodCompletion,
143
- ...modelAttributeCompletion
150
+ ...modelAttributeCompletion,
151
+ ...helperCompletion,
152
+ ...helperInputCompletion
144
153
  ].filter(Boolean)
145
154
 
146
155
  if (completions) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sailshq/language-server",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Language Server Protocol server for Sails Language Service",
5
5
  "homepage": "https://sailjs.com",
6
6
  "repository": {
@@ -20,6 +20,8 @@
20
20
  "author": "Kelvin Omereshone",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
+ "acorn": "^8.14.1",
24
+ "acorn-walk": "^8.3.4",
23
25
  "vscode-languageserver": "^9.0.1",
24
26
  "vscode-languageserver-textdocument": "^1.0.12"
25
27
  },
@@ -4,6 +4,12 @@ const validatePageExist = require('./validate-page-exist')
4
4
  const validateDataTypes = require('./validate-data-type')
5
5
  const validatePolicyExist = require('./validate-policy-exist')
6
6
  const validateModelAttributeExist = require('./validate-model-attribute-exist')
7
+ const validateViewExist = require('./validate-view-exist')
8
+ const validateHelperInputExist = require('./validate-helper-input-exist')
9
+ const validateRequiredHelperInput = require('./validate-required-helper-input')
10
+ const validateRequiredModelAttribute = require('./validate-required-model-attribute')
11
+ const validateModelExist = require('./validate-model-exist')
12
+
7
13
  module.exports = function validateDocument(connection, document, typeMap) {
8
14
  const diagnostics = []
9
15
 
@@ -16,13 +22,29 @@ module.exports = function validateDocument(connection, document, typeMap) {
16
22
  document,
17
23
  typeMap
18
24
  )
25
+ const viewDiagnostics = validateViewExist(document, typeMap)
26
+ const helperInputDiagnostics = validateHelperInputExist(document, typeMap)
27
+ const requiredHelperInputDiagnostics = validateRequiredHelperInput(
28
+ document,
29
+ typeMap
30
+ )
31
+ const requiredModelAttributeDiagnostics = validateRequiredModelAttribute(
32
+ document,
33
+ typeMap
34
+ )
35
+ const modelExistDiagnostics = validateModelExist(document, typeMap)
19
36
  diagnostics.push(
20
37
  ...modelDiagnostics,
21
38
  ...actionDiagnostics,
22
39
  ...pageDiagnostics,
23
40
  ...dataTypeDiagnostics,
24
41
  ...policyDiagnostics,
25
- ...modelAttributeDiagnostics
42
+ ...modelAttributeDiagnostics,
43
+ ...viewDiagnostics,
44
+ ...helperInputDiagnostics,
45
+ ...requiredHelperInputDiagnostics,
46
+ ...requiredModelAttributeDiagnostics,
47
+ ...modelExistDiagnostics
26
48
  )
27
49
 
28
50
  connection.sendDiagnostics({ uri: document.uri, diagnostics })
@@ -0,0 +1,42 @@
1
+ const lsp = require('vscode-languageserver/node')
2
+
3
+ module.exports = function validateHelperInputExist(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
+ while ((propMatch = propsRegex.exec(match[2])) !== null) {
23
+ const key = propMatch[1]
24
+ if (!Object.prototype.hasOwnProperty.call(helperInfo.inputs, key)) {
25
+ const start = match.index + match[0].indexOf(key)
26
+ const end = start + key.length
27
+ diagnostics.push(
28
+ lsp.Diagnostic.create(
29
+ lsp.Range.create(
30
+ document.positionAt(start),
31
+ document.positionAt(end)
32
+ ),
33
+ `Unknown input property '${key}' for helper '${fullHelperName}'.`,
34
+ lsp.DiagnosticSeverity.Error,
35
+ 'sails-lsp'
36
+ )
37
+ )
38
+ }
39
+ }
40
+ }
41
+ return diagnostics
42
+ }