-
Notifications
You must be signed in to change notification settings - Fork 57
Expand file tree
/
Copy pathScope.ts
More file actions
1024 lines (920 loc) · 41.6 KB
/
Scope.ts
File metadata and controls
1024 lines (920 loc) · 41.6 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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import type { CompletionItem, Position, Range } from 'vscode-languageserver';
import * as path from 'path';
import { CompletionItemKind, Location } from 'vscode-languageserver';
import chalk from 'chalk';
import type { DiagnosticInfo } from './DiagnosticMessages';
import { DiagnosticMessages } from './DiagnosticMessages';
import type { CallableContainer, BsDiagnostic, FileReference, BscFile, CallableContainerMap, FileLink } from './interfaces';
import type { Program } from './Program';
import { BsClassValidator } from './validators/ClassValidator';
import type { NamespaceStatement, Statement, FunctionStatement, ClassStatement } from './parser/Statement';
import type { NewExpression } from './parser/Expression';
import { ParseMode } from './parser/Parser';
import { standardizePath as s, util } from './util';
import { globalCallableMap } from './globalCallables';
import { Cache } from './Cache';
import { URI } from 'vscode-uri';
import { LogLevel } from './Logger';
import { isBrsFile, isClassStatement, isFunctionStatement, isFunctionType, isXmlFile, isCustomType, isClassMethodStatement } from './astUtils/reflection';
import type { BrsFile } from './files/BrsFile';
import type { DependencyGraph, DependencyChangedEvent } from './DependencyGraph';
/**
* A class to keep track of all declarations within a given scope (like source scope, component scope)
*/
export class Scope {
constructor(
public name: string,
public program: Program,
private _dependencyGraphKey?: string
) {
this.isValidated = false;
//used for improved logging performance
this._debugLogComponentName = `Scope '${chalk.redBright(this.name)}'`;
}
/**
* Indicates whether this scope needs to be validated.
* Will be true when first constructed, or anytime one of its dependencies changes
*/
public readonly isValidated: boolean;
protected cache = new Cache();
public get dependencyGraphKey() {
return this._dependencyGraphKey;
}
/**
* A dictionary of namespaces, indexed by the lower case full name of each namespace.
* If a namespace is declared as "NameA.NameB.NameC", there will be 3 entries in this dictionary,
* "namea", "namea.nameb", "namea.nameb.namec"
*/
public get namespaceLookup() {
return this.cache.getOrAdd('namespaceLookup', () => this.buildNamespaceLookup());
}
/**
* Get the class with the specified name.
* @param className - The class name, including the namespace of the class if possible
* @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
*/
public getClass(className: string, containingNamespace?: string): ClassStatement {
return this.getClassFileLink(className, containingNamespace)?.item;
}
/**
* Get a class and its containing file by the class name
* @param className - The class name, including the namespace of the class if possible
* @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
*/
public getClassFileLink(className: string, containingNamespace?: string): FileLink<ClassStatement> {
const lowerClassName = className?.toLowerCase();
const classMap = this.getClassMap();
let cls = classMap.get(
util.getFullyQualifiedClassName(lowerClassName, containingNamespace?.toLowerCase())
);
//if we couldn't find the class by its full namespaced name, look for a global class with that name
if (!cls) {
cls = classMap.get(lowerClassName);
}
return cls;
}
/**
* Tests if a class exists with the specified name
* @param className - the all-lower-case namespace-included class name
* @param containingNamespace - The namespace used to resolve relative class names. (i.e. the namespace around the current statement trying to find a class)
*/
public hasClass(className: string, namespaceName?: string): boolean {
return !!this.getClass(className, namespaceName);
}
/**
* A dictionary of all classes in this scope. This includes namespaced classes always with their full name.
* The key is stored in lower case
*/
public getClassMap(): Map<string, FileLink<ClassStatement>> {
return this.cache.getOrAdd('classMap', () => {
const map = new Map<string, FileLink<ClassStatement>>();
this.enumerateBrsFiles((file) => {
if (isBrsFile(file)) {
for (let cls of file.parser.references.classStatements) {
const lowerClassName = cls.getName(ParseMode.BrighterScript)?.toLowerCase();
//only track classes with a defined name (i.e. exclude nameless malformed classes)
if (lowerClassName) {
map.set(lowerClassName, { item: cls, file: file });
}
}
}
});
return map;
});
}
/**
* The list of diagnostics found specifically for this scope. Individual file diagnostics are stored on the files themselves.
*/
protected diagnostics = [] as BsDiagnostic[];
protected onDependenciesChanged(event: DependencyChangedEvent) {
this.logDebug('invalidated because dependency graph said [', event.sourceKey, '] changed');
this.invalidate();
}
/**
* Clean up all event handles
*/
public dispose() {
this.unsubscribeFromDependencyGraph?.();
}
/**
* Does this scope know about the given namespace name?
* @param namespaceName - the name of the namespace (i.e. "NameA", or "NameA.NameB", etc...)
*/
public isKnownNamespace(namespaceName: string) {
let namespaceNameLower = namespaceName.toLowerCase();
this.enumerateBrsFiles((file) => {
for (let namespace of file.parser.references.namespaceStatements) {
let loopNamespaceNameLower = namespace.name.toLowerCase();
if (loopNamespaceNameLower === namespaceNameLower || loopNamespaceNameLower.startsWith(namespaceNameLower + '.')) {
return true;
}
}
});
return false;
}
/**
* Get the parent scope for this scope (for source scope this will always be the globalScope).
* XmlScope overrides this to return the parent xml scope if available.
* For globalScope this will return null.
*/
public getParentScope() {
let scope: Scope;
//use the global scope if we didn't find a sope and this is not the global scope
if (this.program.globalScope !== this) {
scope = this.program.globalScope;
}
if (scope) {
return scope;
} else {
//passing null to the cache allows it to skip the factory function in the future
return null;
}
}
private dependencyGraph: DependencyGraph;
/**
* An unsubscribe function for the dependencyGraph subscription
*/
private unsubscribeFromDependencyGraph: () => void;
public attachDependencyGraph(dependencyGraph: DependencyGraph) {
this.dependencyGraph = dependencyGraph;
if (this.unsubscribeFromDependencyGraph) {
this.unsubscribeFromDependencyGraph();
}
//anytime a dependency for this scope changes, we need to be revalidated
this.unsubscribeFromDependencyGraph = this.dependencyGraph.onchange(this.dependencyGraphKey, this.onDependenciesChanged.bind(this));
//invalidate immediately since this is a new scope
this.invalidate();
}
/**
* Get the file with the specified pkgPath
*/
public getFile(pathAbsolute: string) {
pathAbsolute = s`${pathAbsolute}`;
let files = this.getAllFiles();
for (let file of files) {
if (file.pathAbsolute === pathAbsolute) {
return file;
}
}
}
/**
* Get the list of files referenced by this scope that are actually loaded in the program.
* Excludes files from ancestor scopes
*/
public getOwnFiles() {
//source scope only inherits files from global, so just return all files. This function mostly exists to assist XmlScope
return this.getAllFiles();
}
/**
* Get the list of files referenced by this scope that are actually loaded in the program.
* Includes files from this scope and all ancestor scopes
*/
public getAllFiles() {
return this.cache.getOrAdd('getAllFiles', () => {
let result = [] as BscFile[];
let dependencies = this.dependencyGraph.getAllDependencies(this.dependencyGraphKey);
for (let dependency of dependencies) {
//load components by their name
if (dependency.startsWith('component:')) {
let comp = this.program.getComponent(dependency.replace(/$component:/, ''));
if (comp) {
result.push(comp.file);
}
} else {
let file = this.program.getFileByPkgPath(dependency);
if (file) {
result.push(file);
}
}
}
this.logDebug('getAllFiles', () => result.map(x => x.pkgPath));
return result;
});
}
/**
* Get the list of errors for this scope. It's calculated on the fly, so
* call this sparingly.
*/
public getDiagnostics() {
let diagnosticLists = [this.diagnostics] as BsDiagnostic[][];
//add diagnostics from every referenced file
this.enumerateOwnFiles((file) => {
diagnosticLists.push(file.getDiagnostics());
});
let allDiagnostics = Array.prototype.concat.apply([], diagnosticLists) as BsDiagnostic[];
let filteredDiagnostics = allDiagnostics.filter((x) => {
return !util.diagnosticIsSuppressed(x);
});
//filter out diangostics that match any of the comment flags
return filteredDiagnostics;
}
public addDiagnostics(diagnostics: BsDiagnostic[]) {
this.diagnostics.push(...diagnostics);
}
/**
* Get the list of callables available in this scope (either declared in this scope or in a parent scope)
*/
public getAllCallables(): CallableContainer[] {
//get callables from parent scopes
let parentScope = this.getParentScope();
if (parentScope) {
return [...this.getOwnCallables(), ...parentScope.getAllCallables()];
} else {
return [...this.getOwnCallables()];
}
}
/**
* Get the callable with the specified name.
* If there are overridden callables with the same name, the closest callable to this scope is returned
* @param name
*/
public getCallableByName(name: string) {
let lowerName = name.toLowerCase();
let callables = this.getAllCallables();
for (let callable of callables) {
if (callable.callable.getName(ParseMode.BrighterScript).toLowerCase() === lowerName) {
return callable.callable;
}
}
}
/**
* Iterate over Brs files not shadowed by typedefs
*/
public enumerateBrsFiles(callback: (file: BrsFile) => void) {
const files = this.getAllFiles();
for (const file of files) {
//only brs files without a typedef
if (isBrsFile(file) && !file.hasTypedef) {
callback(file);
}
}
}
/**
* Call a function for each file directly included in this scope (excluding files found only in parent scopes).
*/
public enumerateOwnFiles(callback: (file: BscFile) => void) {
const files = this.getOwnFiles();
for (const file of files) {
//either XML components or files without a typedef
if (isXmlFile(file) || !file.hasTypedef) {
callback(file);
}
}
}
/**
* Get the list of callables explicitly defined in files in this scope.
* This excludes ancestor callables
*/
public getOwnCallables(): CallableContainer[] {
let result = [] as CallableContainer[];
this.logDebug('getOwnCallables() files: ', () => this.getOwnFiles().map(x => x.pkgPath));
//get callables from own files
this.enumerateOwnFiles((file) => {
for (let callable of file.callables) {
result.push({
callable: callable,
scope: this
});
}
});
return result;
}
/**
* Builds a tree of namespace objects
*/
public buildNamespaceLookup() {
let namespaceLookup = new Map<string, NamespaceContainer>();
this.enumerateBrsFiles((file) => {
for (let namespace of file.parser.references.namespaceStatements) {
//TODO should we handle non-brighterscript?
let name = namespace.nameExpression.getName(ParseMode.BrighterScript);
let nameParts = name.split('.');
let loopName = null;
//ensure each namespace section is represented in the results
//(so if the namespace name is A.B.C, this will make an entry for "A", an entry for "A.B", and an entry for "A.B.C"
for (let part of nameParts) {
loopName = loopName === null ? part : `${loopName}.${part}`;
let lowerLoopName = loopName.toLowerCase();
if (!namespaceLookup.has(lowerLoopName)) {
namespaceLookup.set(lowerLoopName, {
file: file,
fullName: loopName,
nameRange: namespace.nameExpression.range,
lastPartName: part,
namespaces: new Map<string, NamespaceContainer>(),
classStatements: {},
functionStatements: {},
statements: []
});
}
}
let ns = namespaceLookup.get(name.toLowerCase());
ns.statements.push(...namespace.body.statements);
for (let statement of namespace.body.statements) {
if (isClassStatement(statement) && statement.name) {
ns.classStatements[statement.name.text.toLowerCase()] = statement;
} else if (isFunctionStatement(statement) && statement.name) {
ns.functionStatements[statement.name.text.toLowerCase()] = statement;
}
}
}
//associate child namespaces with their parents
for (let [, ns] of namespaceLookup) {
let parts = ns.fullName.split('.');
if (parts.length > 1) {
//remove the last part
parts.pop();
let parentName = parts.join('.');
const parent = namespaceLookup.get(parentName.toLowerCase());
parent.namespaces.set(ns.lastPartName.toLowerCase(), ns);
}
}
});
return namespaceLookup;
}
public getAllNamespaceStatements() {
let result = [] as NamespaceStatement[];
this.enumerateBrsFiles((file) => {
result.push(...file.parser.references.namespaceStatements);
});
return result;
}
protected logDebug(...args: any[]) {
this.program.logger.debug(this._debugLogComponentName, ...args);
}
private _debugLogComponentName: string;
public validate(force = false) {
//if this scope is already validated, no need to revalidate
if (this.isValidated === true && !force) {
this.logDebug('validate(): already validated');
return;
}
this.program.logger.time(LogLevel.debug, [this._debugLogComponentName, 'validate()'], () => {
let parentScope = this.getParentScope();
//validate our parent before we validate ourself
if (parentScope && parentScope.isValidated === false) {
this.logDebug('validate(): validating parent first');
parentScope.validate(force);
}
//clear the scope's errors list (we will populate them from this method)
this.diagnostics = [];
let callables = this.getAllCallables();
//sort the callables by filepath and then method name, so the errors will be consistent
callables = callables.sort((a, b) => {
return (
//sort by path
a.callable.file.pathAbsolute.localeCompare(b.callable.file.pathAbsolute) ||
//then sort by method name
a.callable.name.localeCompare(b.callable.name)
);
});
//get a list of all callables, indexed by their lower case names
let callableContainerMap = util.getCallableContainersByLowerName(callables);
let files = this.getOwnFiles();
this.program.plugins.emit('beforeScopeValidate', this, files, callableContainerMap);
this.program.plugins.emit('onScopeValidate', {
program: this.program,
scope: this
});
this._validate(callableContainerMap);
this.program.plugins.emit('afterScopeValidate', this, files, callableContainerMap);
(this as any).isValidated = true;
});
}
protected _validate(callableContainerMap: CallableContainerMap) {
//find all duplicate function declarations
this.diagnosticFindDuplicateFunctionDeclarations(callableContainerMap);
//detect missing and incorrect-case script imports
this.diagnosticValidateScriptImportPaths();
//enforce a series of checks on the bodies of class methods
this.validateClasses();
//do many per-file checks
this.enumerateBrsFiles((file) => {
this.diagnosticDetectCallsToUnknownFunctions(file, callableContainerMap);
this.diagnosticDetectFunctionCallsWithWrongParamCount(file, callableContainerMap);
this.diagnosticDetectShadowedLocalVars(file, callableContainerMap);
this.diagnosticDetectFunctionCollisions(file);
this.detectVariableNamespaceCollisions(file);
this.diagnosticDetectInvalidFunctionExpressionTypes(file);
});
}
/**
* Mark this scope as invalid, which means its `validate()` function needs to be called again before use.
*/
public invalidate() {
(this as any).isValidated = false;
//clear out various lookups (they'll get regenerated on demand the next time they're requested)
this.cache.clear();
}
private detectVariableNamespaceCollisions(file: BrsFile) {
//find all function parameters
for (let func of file.parser.references.functionExpressions) {
for (let param of func.parameters) {
let lowerParamName = param.name.text.toLowerCase();
let namespace = this.namespaceLookup.get(lowerParamName);
//see if the param matches any starting namespace part
if (namespace) {
this.diagnostics.push({
file: file,
...DiagnosticMessages.parameterMayNotHaveSameNameAsNamespace(param.name.text),
range: param.name.range,
relatedInformation: [{
message: 'Namespace declared here',
location: Location.create(
URI.file(namespace.file.pathAbsolute).toString(),
namespace.nameRange
)
}]
});
}
}
}
for (let assignment of file.parser.references.assignmentStatements) {
let lowerAssignmentName = assignment.name.text.toLowerCase();
let namespace = this.namespaceLookup.get(lowerAssignmentName);
//see if the param matches any starting namespace part
if (namespace) {
this.diagnostics.push({
file: file,
...DiagnosticMessages.variableMayNotHaveSameNameAsNamespace(assignment.name.text),
range: assignment.name.range,
relatedInformation: [{
message: 'Namespace declared here',
location: Location.create(
URI.file(namespace.file.pathAbsolute).toString(),
namespace.nameRange
)
}]
});
}
}
}
/**
* Find various function collisions
*/
private diagnosticDetectFunctionCollisions(file: BscFile) {
for (let func of file.callables) {
const funcName = func.getName(ParseMode.BrighterScript);
const lowerFuncName = funcName?.toLowerCase();
if (lowerFuncName) {
//find function declarations with the same name as a stdlib function
if (globalCallableMap.has(lowerFuncName)) {
this.diagnostics.push({
...DiagnosticMessages.scopeFunctionShadowedByBuiltInFunction(),
range: func.nameRange,
file: file
});
}
//find any functions that have the same name as a class
if (this.hasClass(lowerFuncName)) {
this.diagnostics.push({
...DiagnosticMessages.functionCannotHaveSameNameAsClass(funcName),
range: func.nameRange,
file: file
});
}
}
}
}
/**
* Find function parameters and function return types that are neither built-in types or known Class references
*/
private diagnosticDetectInvalidFunctionExpressionTypes(file: BrsFile) {
for (let func of file.parser.references.functionExpressions) {
if (isCustomType(func.returnType) && func.returnTypeToken) {
// check if this custom type is in our class map
const returnTypeName = func.returnType.name;
const currentNamespaceName = func.namespaceName?.getName(ParseMode.BrighterScript);
if (!this.hasClass(returnTypeName, currentNamespaceName)) {
this.diagnostics.push({
...DiagnosticMessages.invalidFunctionReturnType(returnTypeName),
range: func.returnTypeToken.range,
file: file
});
}
}
for (let param of func.parameters) {
if (isCustomType(param.type) && param.typeToken) {
const paramTypeName = param.type.name;
const currentNamespaceName = func.namespaceName?.getName(ParseMode.BrighterScript);
if (!this.hasClass(paramTypeName, currentNamespaceName)) {
this.diagnostics.push({
...DiagnosticMessages.functionParameterTypeIsInvalid(param.name.text, paramTypeName),
range: param.typeToken.range,
file: file
});
}
}
}
}
}
public getNewExpressions() {
let result = [] as AugmentedNewExpression[];
this.enumerateBrsFiles((file) => {
let expressions = file.parser.references.newExpressions as AugmentedNewExpression[];
for (let expression of expressions) {
expression.file = file;
result.push(expression);
}
});
return result;
}
private validateClasses() {
let validator = new BsClassValidator();
validator.validate(this);
this.diagnostics.push(...validator.diagnostics);
}
/**
* Detect calls to functions with the incorrect number of parameters
* @param file
* @param callableContainersByLowerName
*/
private diagnosticDetectFunctionCallsWithWrongParamCount(file: BscFile, callableContainersByLowerName: CallableContainerMap) {
//validate all function calls
for (let expCall of file.functionCalls) {
let callableContainersWithThisName = callableContainersByLowerName.get(expCall.name.toLowerCase());
//use the first item from callablesByLowerName, because if there are more, that's a separate error
let knownCallableContainer = callableContainersWithThisName ? callableContainersWithThisName[0] : undefined;
if (knownCallableContainer) {
//get min/max parameter count for callable
let minParams = 0;
let maxParams = 0;
for (let param of knownCallableContainer.callable.params) {
maxParams++;
//optional parameters must come last, so we can assume that minParams won't increase once we hit
//the first isOptional
if (param.isOptional !== true) {
minParams++;
}
}
let expCallArgCount = expCall.args.length;
if (expCall.args.length > maxParams || expCall.args.length < minParams) {
let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`;
this.diagnostics.push({
...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount),
range: expCall.nameRange,
//TODO detect end of expression call
file: file
});
}
}
}
}
/**
* Detect local variables (function scope) that have the same name as scope calls
* @param file
* @param callableContainerMap
*/
private diagnosticDetectShadowedLocalVars(file: BscFile, callableContainerMap: CallableContainerMap) {
const classMap = this.getClassMap();
//loop through every function scope
for (let scope of file.functionScopes) {
//every var declaration in this function scope
for (let varDeclaration of scope.variableDeclarations) {
const varName = varDeclaration.name;
const lowerVarName = varName.toLowerCase();
//if the var is a function
if (isFunctionType(varDeclaration.type)) {
//local var function with same name as stdlib function
if (
//has same name as stdlib
globalCallableMap.has(lowerVarName)
) {
this.diagnostics.push({
...DiagnosticMessages.localVarFunctionShadowsParentFunction('stdlib'),
range: varDeclaration.nameRange,
file: file
});
//this check needs to come after the stdlib one, because the stdlib functions are included
//in the scope function list
} else if (
//has same name as scope function
callableContainerMap.has(lowerVarName)
) {
this.diagnostics.push({
...DiagnosticMessages.localVarFunctionShadowsParentFunction('scope'),
range: varDeclaration.nameRange,
file: file
});
}
//var is not a function
} else if (
//is NOT a callable from stdlib (because non-function local vars can have same name as stdlib names)
!globalCallableMap.has(lowerVarName)
) {
//is same name as a callable
if (callableContainerMap.has(lowerVarName)) {
this.diagnostics.push({
...DiagnosticMessages.localVarShadowedByScopedFunction(),
range: varDeclaration.nameRange,
file: file
});
//has the same name as an in-scope class
} else if (classMap.has(lowerVarName)) {
this.diagnostics.push({
...DiagnosticMessages.localVarSameNameAsClass(classMap.get(lowerVarName)?.item.getName(ParseMode.BrighterScript)),
range: varDeclaration.nameRange,
file: file
});
}
}
}
}
}
/**
* Detect calls to functions that are not defined in this scope
* @param file
* @param callablesByLowerName
*/
private diagnosticDetectCallsToUnknownFunctions(file: BscFile, callablesByLowerName: CallableContainerMap) {
//validate all expression calls
for (let expCall of file.functionCalls) {
const lowerName = expCall.name.toLowerCase();
//for now, skip validation on any method named "super" within `.bs` contexts.
//TODO revise this logic so we know if this function call resides within a class constructor function
if (file.extension === '.bs' && lowerName === 'super') {
continue;
}
//get the local scope for this expression
let scope = file.getFunctionScopeAtPosition(expCall.nameRange.start);
//if we don't already have a variable with this name.
if (!scope?.getVariableByName(lowerName)) {
let callablesWithThisName: CallableContainer[];
if (expCall.functionScope.func.namespaceName) {
// prefer namespaced function
const potentialNamespacedCallable = expCall.functionScope.func.namespaceName.getName(ParseMode.BrightScript).toLowerCase() + '_' + lowerName;
callablesWithThisName = callablesByLowerName.get(potentialNamespacedCallable.toLowerCase());
}
if (!callablesWithThisName) {
// just try it as is
callablesWithThisName = callablesByLowerName.get(lowerName);
}
//use the first item from callablesByLowerName, because if there are more, that's a separate error
let knownCallable = callablesWithThisName ? callablesWithThisName[0] : undefined;
//detect calls to unknown functions
if (!knownCallable) {
this.diagnostics.push({
...DiagnosticMessages.callToUnknownFunction(expCall.name, this.name),
range: expCall.nameRange,
file: file
});
}
} else {
//if we found a variable with the same name as the function, assume the call is "known".
//If the variable is a different type, some other check should add a diagnostic for that.
}
}
}
/**
* Create diagnostics for any duplicate function declarations
* @param callablesByLowerName
*/
private diagnosticFindDuplicateFunctionDeclarations(callableContainersByLowerName: CallableContainerMap) {
//for each list of callables with the same name
for (let [lowerName, callableContainers] of callableContainersByLowerName) {
let globalCallables = [] as CallableContainer[];
let nonGlobalCallables = [] as CallableContainer[];
let ownCallables = [] as CallableContainer[];
let ancestorNonGlobalCallables = [] as CallableContainer[];
for (let container of callableContainers) {
if (container.scope === this.program.globalScope) {
globalCallables.push(container);
} else {
nonGlobalCallables.push(container);
if (container.scope === this) {
ownCallables.push(container);
} else {
ancestorNonGlobalCallables.push(container);
}
}
}
//add info diagnostics about child shadowing parent functions
if (ownCallables.length > 0 && ancestorNonGlobalCallables.length > 0) {
for (let container of ownCallables) {
//skip the init function (because every component will have one of those){
if (lowerName !== 'init') {
let shadowedCallable = ancestorNonGlobalCallables[ancestorNonGlobalCallables.length - 1];
if (!!shadowedCallable && shadowedCallable.callable.file === container.callable.file) {
//same file: skip redundant imports
continue;
}
this.diagnostics.push({
...DiagnosticMessages.overridesAncestorFunction(
container.callable.name,
container.scope.name,
shadowedCallable.callable.file.pkgPath,
//grab the last item in the list, which should be the closest ancestor's version
shadowedCallable.scope.name
),
range: container.callable.nameRange,
file: container.callable.file
});
}
}
}
//add error diagnostics about duplicate functions in the same scope
if (ownCallables.length > 1) {
for (let callableContainer of ownCallables) {
let callable = callableContainer.callable;
this.diagnostics.push({
...DiagnosticMessages.duplicateFunctionImplementation(callable.name, callableContainer.scope.name),
range: util.createRange(
callable.nameRange.start.line,
callable.nameRange.start.character,
callable.nameRange.start.line,
callable.nameRange.end.character
),
file: callable.file
});
}
}
}
}
/**
* Get the list of all script imports for this scope
*/
private getOwnScriptImports() {
let result = [] as FileReference[];
this.enumerateOwnFiles((file) => {
if (isBrsFile(file)) {
result.push(...file.ownScriptImports);
} else if (isXmlFile(file)) {
result.push(...file.scriptTagImports);
}
});
return result;
}
/**
* Verify that all of the scripts imported by each file in this scope actually exist
*/
private diagnosticValidateScriptImportPaths() {
let scriptImports = this.getOwnScriptImports();
//verify every script import
for (let scriptImport of scriptImports) {
let referencedFile = this.getFileByRelativePath(scriptImport.pkgPath);
//if we can't find the file
if (!referencedFile) {
//skip the default bslib file, it will exist at transpile time but should not show up in the program during validation cycle
if (scriptImport.pkgPath === `source${path.sep}bslib.brs`) {
continue;
}
let dInfo: DiagnosticInfo;
if (scriptImport.text.trim().length === 0) {
dInfo = DiagnosticMessages.scriptSrcCannotBeEmpty();
} else {
dInfo = DiagnosticMessages.referencedFileDoesNotExist();
}
this.diagnostics.push({
...dInfo,
range: scriptImport.filePathRange,
file: scriptImport.sourceFile
});
//if the character casing of the script import path does not match that of the actual path
} else if (scriptImport.pkgPath !== referencedFile.pkgPath) {
this.diagnostics.push({
...DiagnosticMessages.scriptImportCaseMismatch(referencedFile.pkgPath),
range: scriptImport.filePathRange,
file: scriptImport.sourceFile
});
}
}
}
/**
* Find the file with the specified relative path
* @param relativePath
*/
protected getFileByRelativePath(relativePath: string) {
if (!relativePath) {
return;
}
let files = this.getAllFiles();
for (let file of files) {
if (file.pkgPath.toLowerCase() === relativePath.toLowerCase()) {
return file;
}
}
}
/**
* Determine if this file is included in this scope (excluding parent scopes)
* @param file
*/
public hasFile(file: BscFile) {
let files = this.getOwnFiles();
let hasFile = files.includes(file);
return hasFile;
}
/**
* Get all callables as completionItems
*/
public getCallablesAsCompletions(parseMode: ParseMode) {
let completions = [] as CompletionItem[];
let callables = this.getAllCallables();
if (parseMode === ParseMode.BrighterScript) {
//throw out the namespaced callables (they will be handled by another method)
callables = callables.filter(x => x.callable.hasNamespace === false);
}
for (let callableContainer of callables) {
completions.push(this.createCompletionFromCallable(callableContainer));
}
return completions;
}
public createCompletionFromCallable(callableContainer: CallableContainer): CompletionItem {
return {
label: callableContainer.callable.getName(ParseMode.BrighterScript),
kind: CompletionItemKind.Function,
detail: callableContainer.callable.shortDescription,
documentation: callableContainer.callable.documentation ? { kind: 'markdown', value: callableContainer.callable.documentation } : undefined
};
}
public createCompletionFromFunctionStatement(statement: FunctionStatement): CompletionItem {
return {
label: statement.getName(ParseMode.BrighterScript),
kind: CompletionItemKind.Function
};
}
/**
* Get the definition (where was this thing first defined) of the symbol under the position
*/
public getDefinition(file: BscFile, position: Position): Location[] {
// Overridden in XMLScope. Brs files use implementation in BrsFile
return [];
}
/**
* Scan all files for property names, and return them as completions
*/
public getPropertyNameCompletions() {
let results = [] as CompletionItem[];
this.enumerateBrsFiles((file) => {
results.push(...file.propertyNameCompletions);
});
return results;
}
public getAllClassMemberCompletions() {
let results = new Map<string, CompletionItem>();
let filesSearched = new Set<BscFile>();
for (const file of this.getAllFiles()) {
if (isXmlFile(file) || filesSearched.has(file)) {
continue;
}
filesSearched.add(file);
for (let cs of file.parser.references.classStatements) {
for (let s of [...cs.methods, ...cs.fields]) {
if (!results.has(s.name.text) && s.name.text.toLowerCase() !== 'new') {
results.set(s.name.text, {
label: s.name.text,
kind: isClassMethodStatement(s) ? CompletionItemKind.Method : CompletionItemKind.Field
});
}
}
}
}
return results;
}
/**
* @param className - The name of the class (including namespace if possible)
* @param callsiteNamespace - the name of the namespace where the call site resides (this is NOT the known namespace of the class).
* This is used to help resolve non-namespaced class names that reside in the same namespac as the call site.
*/
public getClassHierarchy(className: string, callsiteNamespace?: string) {