forked from frizLabz-FFriZz/PineScript-v6-vscode
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathPineParser.ts
More file actions
310 lines (271 loc) · 11 KB
/
Copy pathPineParser.ts
File metadata and controls
310 lines (271 loc) · 11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import { Class } from './index'
import { Helpers } from './PineHelpers'
import { VSCode } from './VSCode'
export class PineParser {
changes: number | undefined
libs: any
libIds: any[] = []
parsedLibsFunctions: any = {}
parsedLibsUDT: any = {}
// Refactored regular expressions with named capture groups for better readability and maintainability
// Type Definition Pattern
typePattern: RegExp =
/(?<udtGroup>(?<annotationsGroup>(^\/\/\s*(?:@(?:type|field)[^\n]*))+(?=^((?:method\s+)?(export\s+)?)?\w+))?((export)?\s*(type)\s*(?<typeName>\w+)\n(?<fieldsGroup>(?:(?:\s+[^\n]+)\n+|\s*\n)+)))(?=(?:\b|^\/\/\s*@|(?:^\/\/[^@\n]*?$)+|$))/gm
// Fields Pattern within Type Definition
fieldsPattern: RegExp =
/^\s+(?:(?:(?:(array|matrix|map)<(?<genericTypes>(?<genericType1>([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)),)?(?<genericType2>([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*)))>)|(?<fieldType>([a-zA-Z_][a-zA-Z_0-9]*\.)?([a-zA-Z_][a-zA-Z_0-9]*))((?<isArray>\[\])?)\s+)?(?<fieldName>[a-zA-Z_][a-zA-Z0-9_]*)(?:(?=\s*=\s*)(?:(?<defaultValueSingleQuote>'.*')|(?<defaultValueDoubleQuote>".*")|(?<defaultValueNumber>\d*(\.(\d+[eE]?\d+)?\d*|\d+))|(?<defaultValueColor>#[a-fA-F0-9]{6,8})|(?<defaultValueIdentifier>([a-zA-Z_][a-zA-Z0-9_]*\.)*[a-zA-Z_][a-zA-Z0-9_]*)))?$/gm
// Function Definition Pattern
funcPattern: RegExp =
/(\/\/\s*@f(?:@?.*\n)+?)?(?<exportKeyword>export)?\s*(?<methodKeyword>method)?\s*(?<functionName>\w+)\s*\(\s*(?<parameters>[^\)]+?)\s*\)\s*?=>\s*?(?<body>(?:.*\n+)+?)(?=^\b|^\/\/\s*\@|$)/gm
// Function Argument Pattern
funcArgPattern: RegExp =
/(?:(?<argModifier>simple|series)?\s+?)?(?<argType>[\w\.\[\]]*?|\w+<[^>]+>)\s*(?<argName>\w+)(?:\s*=\s*(?<argDefaultValue>['"]?[^,)\n]+['"]?)|\s*(?:,|\)|$))/g
// Function Name and Arguments Pattern (currently unused in provided code, but kept for potential future use)
funcNameArgsPattern: RegExp = /([\w.]+)\(([^)]+)\)/g
constructor() {
this.libs = []
}
/**
* Sets the library IDs
* @param libIds - The library IDs
*/
setLibIds(libIds: any) {
if (!Array.isArray(libIds)) {
console.warn('setLibIds: libIds should be an array, received:', libIds)
return // Guard clause for input validation
}
this.libIds = libIds
}
/**
* Parses the libraries by fetching and then parsing functions and types.
* Ensures idempotency by checking if libraries are already parsed.
*/
parseLibs() {
if (!Array.isArray(this.libIds) || this.libIds.length === 0) {
return // Guard clause: No libs to parse
}
this.callLibParser()
}
/**
* Parses the document in VSCode editor.
* It retrieves document text and then parses functions and types.
*/
parseDoc() {
const editorDoc = VSCode.Text?.replace(/\r\n/g, '\n') ?? ''
if (!editorDoc) {
return // Guard clause: No document content to parse
}
const document = [{ script: editorDoc }]
this.callDocParser(document)
}
/**
* Fetches library scripts based on provided library IDs.
* It avoids redundant fetching of the same library.
* @returns Array of library objects with id, alias, and script content.
*/
fetchLibs(): any[] {
if (!Array.isArray(this.libIds) || this.libIds.length === 0) {
return [] // Guard clause: No libIds to fetch
}
const fetchedLibs: any[] = []
for (const lib of this.libIds) {
const { id: libId, alias } = lib
if (!libId || !alias) {
console.warn('fetchLibs: Invalid lib object format:', lib)
continue // Skip invalid lib objects
}
const existingLib = this.libs.find((item: any) => item.id === libId && item.alias === alias)
if (existingLib) {
fetchedLibs.push(existingLib) // Use existing if already fetched
continue
}
Class.PineRequest.libList(libId)
.then((response: any) => {
if (!Array.isArray(response)) {
console.warn('fetchLibs: Unexpected libList response format:', response)
return // Skip if response is not an array
}
for (const libData of response) {
if (!libData?.scriptIdPart || !libData?.version) {
console.warn('fetchLibs: Incomplete libData:', libData)
return // Skip incomplete libData
}
Class.PineRequest.getScript(libData.scriptIdPart, libData.version.replace('.0', ''))
.then((scriptContent: any) => {
if (!scriptContent?.source) {
console.warn('fetchLibs: No script source in scriptContent:', scriptContent)
return // Skip if no script source
}
const scriptString = scriptContent.source.replace(/\r\n/g, '\n')
const libObj = { id: libId, alias: alias, script: scriptString }
fetchedLibs.push(libObj)
})
.catch((scriptError: any) => {
console.error('fetchLibs: Error fetching script:', scriptError)
})
}
})
.catch((listError: any) => {
console.error('fetchLibs: Error fetching lib list:', listError)
})
}
return fetchedLibs
}
/**
* Orchestrates the parsing of fetched libraries.
* It checks if libraries are already parsed to avoid redundant parsing.
*/
callLibParser() {
this.libs = this.fetchLibs()
if (!Array.isArray(this.libs) || this.libs.length === 0) {
return // Guard clause: No libraries fetched to parse
}
for (const lib of this.libs) {
if (this.parsedLibsFunctions?.[lib.alias] || this.parsedLibsUDT?.[lib.alias]) {
continue // Skip if already parsed
}
this.parseFunctions([lib]) // Parse each lib individually
this.parseTypes([lib])
}
}
/**
* Orchestrates the parsing of the document content.
* @param documents - An array of documents to parse, each with a 'script' property.
*/
callDocParser(documents: any[]) {
if (!Array.isArray(documents) || documents.length === 0) {
return // Guard clause: No documents to parse
}
this.parseFunctions(documents)
this.parseTypes(documents)
}
/**
* Parses functions from the provided documents.
* Extracts function name, arguments, and body using regex.
* @param documents - An array of documents to parse, each with a 'script' property.
*/
parseFunctions(documents: any[]) {
if (!Array.isArray(documents)) {
console.error('parseFunctions: Documents must be an array, received:', documents)
return // Guard clause: Validate documents input
}
const parsedFunctions: any[] = []
for (const doc of documents) {
const { script, alias } = doc
if (typeof script !== 'string') {
console.warn('parseFunctions: Script is not a string, skipping:', script)
continue // Guard clause: Skip non-string scripts
}
const functionMatches = script.matchAll(this.funcPattern)
for (const funcMatch of functionMatches) {
const { functionName, parameters, body } = funcMatch.groups! // Non-null assertion is safe due to regex match
const name = (alias ? alias + '.' : '') + functionName
const functionBuild: any = {
name: name,
args: [],
originalName: functionName,
body: body,
}
const funcParamsMatches = parameters.matchAll(this.funcArgPattern)
for (const paramMatch of funcParamsMatches) {
const { argModifier, argType: paramType, argName, argDefaultValue } = paramMatch.groups! // Non-null assertion is safe due to regex match
let resolvedArgType = paramType
if (!resolvedArgType) {
const docMatch = Helpers.checkDocsMatch(argDefaultValue ?? '')
resolvedArgType = docMatch && typeof docMatch === 'string' ? docMatch : resolvedArgType
}
const argsDict: Record<string, any> = {
name: argName,
required: !argDefaultValue,
}
if (argDefaultValue) {
argsDict.default = argDefaultValue
}
if (resolvedArgType) {
argsDict.type = resolvedArgType
}
if (argModifier) {
argsDict.modifier = argModifier // simple | series
}
functionBuild.args.push(argsDict)
}
parsedFunctions.push(functionBuild)
}
if (alias) {
this.parsedLibsFunctions[alias] = parsedFunctions
}
}
Class.PineDocsManager.setParsed(parsedFunctions, 'args')
}
/**
* Parses types (UDTs) from the provided documents.
* Extracts type name and fields using regex.
* @param documents - An array of documents to parse, each with a 'script' property.
*/
parseTypes(documents: any[]) {
if (!Array.isArray(documents)) {
console.error('parseTypes: Documents must be an array, received:', documents)
return // Guard clause: Validate documents input
}
const parsedTypes: any[] = []
for (const doc of documents) {
const { script, alias } = doc
if (typeof script !== 'string') {
console.warn('parseTypes: Script is not a string, skipping:', script)
continue // Guard clause: Skip non-string scripts
}
const typeMatches = script.matchAll(this.typePattern)
for (const typeMatch of typeMatches) {
const { typeName, fieldsGroup } = typeMatch.groups! // Non-null assertion is safe due to regex match
const name = (alias ? alias + '.' : '') + typeName
const typeBuild: any = {
name: name,
fields: [],
originalName: typeName,
}
if (fieldsGroup) {
const fieldMatches = fieldsGroup.matchAll(this.fieldsPattern)
for (const fieldMatch of fieldMatches) {
const {
genericTypes,
genericType1,
genericType2,
fieldType,
isArray,
fieldName,
defaultValueSingleQuote,
defaultValueDoubleQuote,
defaultValueNumber,
defaultValueColor,
defaultValueIdentifier,
} = fieldMatch.groups! // Non-null assertion is safe due to regex match
let resolvedFieldType = genericTypes
? `${fieldMatch[1] /* array|matrix|map */}<${genericType1 || ''}${
genericType1 && genericType2 ? ',' : ''
}${genericType2 || ''}>`
: fieldType + (isArray || '')
const fieldValue =
defaultValueSingleQuote ||
defaultValueDoubleQuote ||
defaultValueNumber ||
defaultValueColor ||
defaultValueIdentifier
const fieldsDict: Record<string, any> = {
name: fieldName,
type: resolvedFieldType,
}
if (fieldValue) {
fieldsDict.default = fieldValue
}
typeBuild.fields.push(fieldsDict)
}
}
parsedTypes.push(typeBuild)
}
if (alias) {
this.parsedLibsUDT[alias] = parsedTypes
}
}
Class.PineDocsManager.setParsed(parsedTypes, 'fields')
}
}