diff --git a/.gitignore b/.gitignore
index 36e7754..c2dd0de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,6 @@ jspm_packages/
# MacOS folder atttribute tracking
**/.DS_Store
+
+**/.sfdx
+**.sf
\ No newline at end of file
diff --git a/grammars/apex.tmLanguage b/grammars/apex.tmLanguage
index c82768f..a9d52cc 100644
--- a/grammars/apex.tmLanguage
+++ b/grammars/apex.tmLanguage
@@ -175,6 +175,10 @@
include
#method-declaration
+
+ include
+ #initializer-block
+
include
#punctuation-semicolon
@@ -309,6 +313,10 @@
include
#comment
+
+ include
+ #dml-expression
+
include
#merge-expression
@@ -693,6 +701,32 @@
+ dml-expression
+
+ begin
+ \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b)
+ beginCaptures
+
+ 1
+
+ name
+ support.function.apex
+
+
+ end
+ (?<=\;)
+ patterns
+
+
+ include
+ #expression
+
+
+ include
+ #punctuation-semicolon
+
+
+
merge-expression
begin
@@ -1328,7 +1362,7 @@
extends-class
begin
- (extends)\b\s+([_[:alpha:]][_[:alnum:]]*)
+ (extends)\b\s+
beginCaptures
1
@@ -1336,14 +1370,41 @@
name
keyword.other.extends.apex
- 2
-
- name
- entity.name.type.extends.apex
-
end
(?={|implements)
+ patterns
+
+
+ begin
+ (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end
+ (?={|implements)
+ patterns
+
+
+ include
+ #support-type
+
+
+ include
+ #type
+
+
+
+
+ match
+ ([_[:alpha:]][_[:alnum:]]*)
+ captures
+
+ 1
+
+ name
+ entity.name.type.extends.apex
+
+
+
+
implements-class
@@ -1359,6 +1420,23 @@
patterns
+
+ begin
+ (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end
+ (?={|extends|,)
+ patterns
+
+
+ include
+ #support-type
+
+
+ include
+ #type
+
+
+
match
([_[:alpha:]][_[:alnum:]]*)\b\s*(,)?
@@ -1376,6 +1454,10 @@
+
+ include
+ #punctuation-comma
+
end
(?={|extends)
@@ -2436,6 +2518,36 @@
+ initializer-block
+
+ begin
+ \{
+ beginCaptures
+
+ 0
+
+ name
+ punctuation.curlybrace.open.apex
+
+
+ end
+ \}
+ endCaptures
+
+ 0
+
+ name
+ punctuation.curlybrace.close.apex
+
+
+ patterns
+
+
+ include
+ #statement
+
+
+
variable-initializer
begin
@@ -2761,7 +2873,7 @@
when-statement
begin
- (when)\b\s+([\'_\-[:alnum:]]+)\s*
+ (when)\b\s+([_\-[:alnum:]]+)\s*
beginCaptures
1
@@ -2781,7 +2893,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2797,7 +2909,7 @@
when-string
begin
- (when)\b\s*('[^'\n]*')(,)?
+ (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s*
beginCaptures
1
@@ -2815,7 +2927,7 @@
- 3
+ 4
patterns
@@ -2825,9 +2937,19 @@
+ 5
+
+ patterns
+
+
+ include
+ #string-literal
+
+
+
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2858,7 +2980,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2884,7 +3006,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2895,6 +3017,10 @@
include
#expression
+
+ include
+ #punctuation-comma
+
when-sobject-statement
@@ -2920,7 +3046,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -4251,7 +4377,7 @@
match
(?x)
-(?:(?:\b(this)\b)\s+)?
+(?:(?:\b(this|final)\b)\s+)?
(?<type_name>
(?:
(?:ref\s+)? # ref return
@@ -4438,7 +4564,7 @@
type-builtin
match
- \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b
+ \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b
captures
1
diff --git a/grammars/apex.tmLanguage.cson b/grammars/apex.tmLanguage.cson
index d7a91db..4accb9c 100644
--- a/grammars/apex.tmLanguage.cson
+++ b/grammars/apex.tmLanguage.cson
@@ -119,6 +119,9 @@ repository:
{
include: '#method-declaration'
}
+ {
+ include: '#initializer-block'
+ }
{
include: '#punctuation-semicolon'
}
@@ -215,6 +218,9 @@ repository:
{
include: '#comment'
}
+ {
+ include: '#dml-expression'
+ }
{
include: '#merge-expression'
}
@@ -433,6 +439,20 @@ repository:
include: '#punctuation-comma'
}
]
+ 'dml-expression':
+ begin: '\\b(delete|insert|undelete|update|upsert)\\b\\s+(?!new\\b)'
+ beginCaptures:
+ '1':
+ name: 'support.function.apex'
+ end: '(?<=\\;)'
+ patterns: [
+ {
+ include: '#expression'
+ }
+ {
+ include: '#punctuation-semicolon'
+ }
+ ]
'merge-expression':
begin: '(merge)\\b\\s+'
beginCaptures:
@@ -797,19 +817,49 @@ repository:
}
]
'extends-class':
- begin: '(extends)\\b\\s+([_[:alpha:]][_[:alnum:]]*)'
+ begin: '(extends)\\b\\s+'
beginCaptures:
'1':
name: 'keyword.other.extends.apex'
- '2':
- name: 'entity.name.type.extends.apex'
end: '(?={|implements)'
+ patterns: [
+ {
+ begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)'
+ end: '(?={|implements)'
+ patterns: [
+ {
+ include: '#support-type'
+ }
+ {
+ include: '#type'
+ }
+ ]
+ }
+ {
+ match: '([_[:alpha:]][_[:alnum:]]*)'
+ captures:
+ '1':
+ name: 'entity.name.type.extends.apex'
+ }
+ ]
'implements-class':
begin: '(implements)\\b'
beginCaptures:
'1':
name: 'keyword.other.implements.apex'
patterns: [
+ {
+ begin: '(?=[_[:alpha:]][_[:alnum:]]*\\s*\\.)'
+ end: '(?={|extends|,)'
+ patterns: [
+ {
+ include: '#support-type'
+ }
+ {
+ include: '#type'
+ }
+ ]
+ }
{
match: '([_[:alpha:]][_[:alnum:]]*)\\b\\s*(,)?'
captures:
@@ -818,6 +868,9 @@ repository:
'2':
name: 'punctuation.separator.comma.apex'
}
+ {
+ include: '#punctuation-comma'
+ }
]
end: '(?={|extends)'
'soql-query-expression':
@@ -1454,6 +1507,20 @@ repository:
include: '#statement'
}
]
+ 'initializer-block':
+ begin: '\\{'
+ beginCaptures:
+ '0':
+ name: 'punctuation.curlybrace.open.apex'
+ end: '\\}'
+ endCaptures:
+ '0':
+ name: 'punctuation.curlybrace.close.apex'
+ patterns: [
+ {
+ include: '#statement'
+ }
+ ]
'variable-initializer':
begin: '(?)'
beginCaptures:
@@ -1627,7 +1694,7 @@ repository:
}
]
'when-statement':
- begin: "(when)\\b\\s+([\\'_\\-[:alnum:]]+)\\s*"
+ begin: '(when)\\b\\s+([_\\-[:alnum:]]+)\\s*'
beginCaptures:
'1':
name: 'keyword.control.switch.when.apex'
@@ -1637,7 +1704,7 @@ repository:
include: '#expression'
}
]
- end: '(?<=\\})'
+ end: '(?=\\})|(?=when\\b)'
patterns: [
{
include: '#block'
@@ -1647,7 +1714,7 @@ repository:
}
]
'when-string':
- begin: "(when)\\b\\s*('[^'\\n]*')(,)?"
+ begin: "(when)\\b\\s*('[^'\\n]*')(\\s*(,)\\s*('[^'\\n]*'))*\\s*"
beginCaptures:
'1':
name: 'keyword.control.switch.when.apex'
@@ -1657,13 +1724,19 @@ repository:
include: '#string-literal'
}
]
- '3':
+ '4':
patterns: [
{
include: '#punctuation-comma'
}
]
- end: '(?<=\\})'
+ '5':
+ patterns: [
+ {
+ include: '#string-literal'
+ }
+ ]
+ end: '(?=\\})|(?=when\\b)'
patterns: [
{
include: '#block'
@@ -1679,7 +1752,7 @@ repository:
name: 'keyword.control.switch.when.apex'
'2':
name: 'keyword.control.switch.else.apex'
- end: '(?<=\\})'
+ end: '(?=\\})|(?=when\\b)'
patterns: [
{
include: '#block'
@@ -1693,7 +1766,7 @@ repository:
beginCaptures:
'1':
name: 'keyword.control.switch.when.apex'
- end: '(?<=\\})'
+ end: '(?=\\})|(?=when\\b)'
patterns: [
{
include: '#block'
@@ -1701,6 +1774,9 @@ repository:
{
include: '#expression'
}
+ {
+ include: '#punctuation-comma'
+ }
]
'when-sobject-statement':
begin: '(when)\\b\\s+([_[:alnum:]]+)\\s+([_[:alnum:]]+)\\s*'
@@ -1711,7 +1787,7 @@ repository:
name: 'storage.type.apex'
'3':
name: 'entity.name.variable.local.apex'
- end: '(?<=\\})'
+ end: '(?=\\})|(?=when\\b)'
patterns: [
{
include: '#block'
@@ -2533,7 +2609,7 @@ repository:
parameter:
match: '''
(?x)
- (?:(?:\\b(this)\\b)\\s+)?
+ (?:(?:\\b(this|final)\\b)\\s+)?
(?
(?:
(?:ref\\s+)? # ref return
@@ -2644,7 +2720,7 @@ repository:
}
]
'type-builtin':
- match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\\b'
+ match: '\\b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\\b'
captures:
'1':
name: 'keyword.type.apex'
diff --git a/grammars/soql.tmLanguage b/grammars/soql.tmLanguage
index e768771..f679de1 100644
--- a/grammars/soql.tmLanguage
+++ b/grammars/soql.tmLanguage
@@ -161,6 +161,10 @@
include
#method-declaration
+
+ include
+ #initializer-block
+
include
#punctuation-semicolon
@@ -295,6 +299,10 @@
include
#comment
+
+ include
+ #dml-expression
+
include
#merge-expression
@@ -679,6 +687,32 @@
+ dml-expression
+
+ begin
+ \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b)
+ beginCaptures
+
+ 1
+
+ name
+ support.function.apex
+
+
+ end
+ (?<=\;)
+ patterns
+
+
+ include
+ #expression
+
+
+ include
+ #punctuation-semicolon
+
+
+
merge-expression
begin
@@ -1314,7 +1348,7 @@
extends-class
begin
- (extends)\b\s+([_[:alpha:]][_[:alnum:]]*)
+ (extends)\b\s+
beginCaptures
1
@@ -1322,14 +1356,41 @@
name
keyword.other.extends.apex
- 2
-
- name
- entity.name.type.extends.apex
-
end
(?={|implements)
+ patterns
+
+
+ begin
+ (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end
+ (?={|implements)
+ patterns
+
+
+ include
+ #support-type
+
+
+ include
+ #type
+
+
+
+
+ match
+ ([_[:alpha:]][_[:alnum:]]*)
+ captures
+
+ 1
+
+ name
+ entity.name.type.extends.apex
+
+
+
+
implements-class
@@ -1345,6 +1406,23 @@
patterns
+
+ begin
+ (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end
+ (?={|extends|,)
+ patterns
+
+
+ include
+ #support-type
+
+
+ include
+ #type
+
+
+
match
([_[:alpha:]][_[:alnum:]]*)\b\s*(,)?
@@ -1362,6 +1440,10 @@
+
+ include
+ #punctuation-comma
+
end
(?={|extends)
@@ -2418,6 +2500,36 @@
+ initializer-block
+
+ begin
+ \{
+ beginCaptures
+
+ 0
+
+ name
+ punctuation.curlybrace.open.apex
+
+
+ end
+ \}
+ endCaptures
+
+ 0
+
+ name
+ punctuation.curlybrace.close.apex
+
+
+ patterns
+
+
+ include
+ #statement
+
+
+
variable-initializer
begin
@@ -2743,7 +2855,7 @@
when-statement
begin
- (when)\b\s+([\'_\-[:alnum:]]+)\s*
+ (when)\b\s+([_\-[:alnum:]]+)\s*
beginCaptures
1
@@ -2763,7 +2875,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2779,7 +2891,7 @@
when-string
begin
- (when)\b\s*('[^'\n]*')(,)?
+ (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s*
beginCaptures
1
@@ -2797,7 +2909,7 @@
- 3
+ 4
patterns
@@ -2807,9 +2919,19 @@
+ 5
+
+ patterns
+
+
+ include
+ #string-literal
+
+
+
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2840,7 +2962,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2866,7 +2988,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -2877,6 +2999,10 @@
include
#expression
+
+ include
+ #punctuation-comma
+
when-sobject-statement
@@ -2902,7 +3028,7 @@
end
- (?<=\})
+ (?=\})|(?=when\b)
patterns
@@ -4233,7 +4359,7 @@
match
(?x)
-(?:(?:\b(this)\b)\s+)?
+(?:(?:\b(this|final)\b)\s+)?
(?<type_name>
(?:
(?:ref\s+)? # ref return
@@ -4420,7 +4546,7 @@
type-builtin
match
- \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b
+ \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b
captures
1
diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml
index 3cfc1bd..5915856 100644
--- a/src/apex.tmLanguage.yml
+++ b/src/apex.tmLanguage.yml
@@ -54,6 +54,7 @@ repository:
- include: '#variable-initializer'
- include: '#constructor-declaration'
- include: '#method-declaration'
+ - include: '#initializer-block'
- include: '#punctuation-semicolon'
interface-members:
@@ -92,6 +93,7 @@ repository:
expression:
patterns:
- include: '#comment'
+ - include: '#dml-expression'
- include: '#merge-expression'
- include: '#support-expression'
- include: '#throw-expression'
@@ -210,6 +212,15 @@ repository:
- include: '#support-type'
- include: '#punctuation-comma'
+ dml-expression:
+ begin: \b(delete|insert|undelete|update|upsert)\b\s+(?!new\b)
+ beginCaptures:
+ '1': { name: support.function.apex }
+ end: (?<=\;)
+ patterns:
+ - include: '#expression'
+ - include: '#punctuation-semicolon'
+
merge-expression:
begin: (merge)\b\s+
beginCaptures:
@@ -410,21 +421,35 @@ repository:
- include: '#comment'
extends-class:
- begin: (extends)\b\s+([_[:alpha:]][_[:alnum:]]*)
+ begin: (extends)\b\s+
beginCaptures:
'1': { name: keyword.other.extends.apex }
- '2': { name: entity.name.type.extends.apex }
end: '(?={|implements)'
+ patterns:
+ - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end: '(?={|implements)'
+ patterns:
+ - include: '#support-type'
+ - include: '#type'
+ - match: ([_[:alpha:]][_[:alnum:]]*)
+ captures:
+ '1': { name: entity.name.type.extends.apex }
implements-class:
begin: (implements)\b
beginCaptures:
'1': { name: keyword.other.implements.apex }
patterns:
+ - begin: (?=[_[:alpha:]][_[:alnum:]]*\s*\.)
+ end: '(?={|extends|,)'
+ patterns:
+ - include: '#support-type'
+ - include: '#type'
- match: ([_[:alpha:]][_[:alnum:]]*)\b\s*(,)?
captures:
'1': { name: entity.name.type.implements.apex }
'2': { name: punctuation.separator.comma.apex }
+ - include: '#punctuation-comma'
end: '(?={|extends)'
soql-query-expression:
@@ -835,6 +860,16 @@ repository:
patterns:
- include: '#statement'
+ initializer-block:
+ begin: \{
+ beginCaptures:
+ '0': { name: punctuation.curlybrace.open.apex }
+ end: \}
+ endCaptures:
+ '0': { name: punctuation.curlybrace.close.apex }
+ patterns:
+ - include: '#statement'
+
variable-initializer:
begin: (?)
beginCaptures:
@@ -944,28 +979,31 @@ repository:
- include: '#punctuation-semicolon'
when-statement:
- begin: (when)\b\s+([\'_\-[:alnum:]]+)\s*
+ begin: (when)\b\s+([_\-[:alnum:]]+)\s*
beginCaptures:
'1': { name: keyword.control.switch.when.apex }
'2':
patterns:
- include: '#expression'
- end: (?<=\})
+ end: (?=\})|(?=when\b)
patterns:
- include: '#block'
- include: '#expression'
when-string:
- begin: (when)\b\s*('[^'\n]*')(,)?
+ begin: (when)\b\s*('[^'\n]*')(\s*(,)\s*('[^'\n]*'))*\s*
beginCaptures:
'1': { name: keyword.control.switch.when.apex }
'2':
patterns:
- include: '#string-literal'
- '3':
+ '4':
patterns:
- include: '#punctuation-comma'
- end: (?<=\})
+ '5':
+ patterns:
+ - include: '#string-literal'
+ end: (?=\})|(?=when\b)
patterns:
- include: '#block'
- include: '#expression'
@@ -975,7 +1013,7 @@ repository:
beginCaptures:
'1': { name: keyword.control.switch.when.apex }
'2': { name: keyword.control.switch.else.apex }
- end: (?<=\})
+ end: (?=\})|(?=when\b)
patterns:
- include: '#block'
- include: '#expression'
@@ -984,10 +1022,11 @@ repository:
begin: (when)\b\s*
beginCaptures:
'1': { name: keyword.control.switch.when.apex }
- end: (?<=\})
+ end: (?=\})|(?=when\b)
patterns:
- include: '#block'
- include: '#expression'
+ - include: '#punctuation-comma'
when-sobject-statement:
begin: (when)\b\s+([_[:alnum:]]+)\s+([_[:alnum:]]+)\s*
@@ -995,7 +1034,7 @@ repository:
'1': { name: keyword.control.switch.when.apex }
'2': { name: storage.type.apex }
'3': { name: entity.name.variable.local.apex }
- end: (?<=\})
+ end: (?=\})|(?=when\b)
patterns:
- include: '#block'
- include: '#expression'
@@ -1644,7 +1683,7 @@ repository:
- include: '#type-nullable-suffix'
type-builtin:
- match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|ID|Integer|Long|Object|String|Time|void)\b
+ match: \b(Blob|Boolean|byte|Date|Datetime|Decimal|Double|Id|ID|Integer|Long|Object|String|Time|void)\b
captures:
'1': { name: keyword.type.apex }
diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts
index 3670bee..7173309 100644
--- a/test/annotation.tests.ts
+++ b/test/annotation.tests.ts
@@ -104,7 +104,7 @@ public class MyTestClass { }`);
it('annotation with multiple parameters on field', async () => {
const input =
Input.InClass(`@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.' required=false)
- global ID leadId;
+ global Id leadId;
`);
const tokens = await tokenize(input);
@@ -133,7 +133,9 @@ public class MyTestClass { }`);
});
it('annotation on same line as method declaration (issue #44)', async () => {
- const input = Input.InClass(`@Future(callout=true) public static void method() {}`);
+ const input = Input.InClass(
+ `@Future(callout=true) public static void method() {}`
+ );
const tokens = await tokenize(input);
tokens.should.deep.equal([
diff --git a/test/class.tests.ts b/test/class.tests.ts
index 69ceb08..7bc4217 100644
--- a/test/class.tests.ts
+++ b/test/class.tests.ts
@@ -216,5 +216,42 @@ public abstract class PublicAbstractClass { }
Token.Punctuation.CloseBrace,
]);
});
+
+ it('class extends namespace-qualified type (issue #50)', async () => {
+ const input = Input.FromText(`class MyClass extends System.Exception {}`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Keywords.Class,
+ Token.Identifiers.ClassName('MyClass'),
+ Token.Keywords.Extends,
+ Token.Support.Class.System,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.TypeText('Exception'),
+ Token.Punctuation.OpenBrace,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
+ it('class implements namespace-qualified type (issue #50)', async () => {
+ const input = Input.FromText(
+ `class MyClass implements Database.Batchable {}`
+ );
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Keywords.Class,
+ Token.Identifiers.ClassName('MyClass'),
+ Token.Keywords.Implements,
+ Token.Support.Class.Database,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.TypeText('Batchable'),
+ Token.Punctuation.TypeParameters.Begin,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Punctuation.OpenBrace,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
});
});
diff --git a/test/expressions.tests.ts b/test/expressions.tests.ts
index 482996e..a63775e 100644
--- a/test/expressions.tests.ts
+++ b/test/expressions.tests.ts
@@ -214,7 +214,9 @@ Object newPoint = new Vector(point.x * z, 0);`);
});
it('ternary with method call (issue #43)', async () => {
- const input = Input.InMethod(`String s = x ? getValue() : getDefault();`);
+ const input = Input.InMethod(
+ `String s = x ? getValue() : getDefault();`
+ );
const tokens = await tokenize(input);
tokens.should.deep.equal([
diff --git a/test/initializer-block.tests.ts b/test/initializer-block.tests.ts
new file mode 100644
index 0000000..44a56cb
--- /dev/null
+++ b/test/initializer-block.tests.ts
@@ -0,0 +1,201 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Modifications Copyright (c) 2018 Salesforce.
+ * See LICENSE in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { should } from 'chai';
+import { tokenize, Input, Token } from './utils/tokenize';
+
+describe('Grammar', () => {
+ before(() => {
+ should();
+ });
+
+ describe('Initializer Blocks', () => {
+ it('empty initialization block', async () => {
+ const input = Input.InClass(`{ }`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Punctuation.OpenBrace,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
+ it('initialization block with method call and string literal', async () => {
+ const input = Input.InClass(`
+{
+ this.setMessage('Object graph should be a Directed Acyclic Graph.');
+}`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Punctuation.OpenBrace,
+ Token.Keywords.This,
+ Token.Punctuation.Accessor,
+ Token.Identifiers.MethodName('setMessage'),
+ Token.Punctuation.OpenParen,
+ Token.Punctuation.String.Begin,
+ Token.Literals.String(
+ 'Object graph should be a Directed Acyclic Graph.'
+ ),
+ Token.Punctuation.String.End,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
+ it('initialization block with multiple statements', async () => {
+ const input = Input.InClass(`
+{
+ Integer x = 5;
+ String message = 'test';
+ this.setMessage(message);
+}`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Punctuation.OpenBrace,
+ Token.PrimitiveType.Integer,
+ Token.Identifiers.LocalName('x'),
+ Token.Operators.Assignment,
+ Token.Literals.Numeric.Decimal('5'),
+ Token.Punctuation.Semicolon,
+ Token.PrimitiveType.String,
+ Token.Identifiers.LocalName('message'),
+ Token.Operators.Assignment,
+ Token.Punctuation.String.Begin,
+ Token.Literals.String('test'),
+ Token.Punctuation.String.End,
+ Token.Punctuation.Semicolon,
+ Token.Keywords.This,
+ Token.Punctuation.Accessor,
+ Token.Identifiers.MethodName('setMessage'),
+ Token.Punctuation.OpenParen,
+ Token.Variables.ReadWrite('message'),
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
+ it('initialization block in nested class (issue #4920)', async () => {
+ const input = Input.FromText(`
+public class TestDataBuilder {
+ public class NoneDAGException extends Exception {
+ // Initializer
+ {
+ this.setMessage('Object graph should be a Directed Acyclic Graph.');
+ }
+
+ // Sample method for comparison
+ public void anotherMethod() {
+ this.setMessage('Object graph should be a Directed Acyclic Graph.');
+ }
+ }
+}`);
+ const tokens = await tokenize(input);
+
+ // Find the initialization block tokens (should start after the comment)
+ const initBlockStart = tokens.findIndex(
+ (t, i) =>
+ i > 0 &&
+ tokens[i - 1].text === '//' &&
+ tokens[i].text === 'Initializer'
+ );
+ const initBlockEnd = tokens.findIndex(
+ (t, i) =>
+ i > initBlockStart && t.text === '}' && tokens[i - 1]?.text === ';'
+ );
+
+ // Extract tokens for the initialization block
+ const initBlockTokens = tokens.slice(
+ initBlockStart + 3, // Skip comment tokens
+ initBlockEnd + 1
+ );
+
+ // Verify initialization block has proper string highlighting
+ initBlockTokens.should.include.deep.members([
+ Token.Punctuation.OpenBrace,
+ Token.Keywords.This,
+ Token.Punctuation.Accessor,
+ Token.Identifiers.MethodName('setMessage'),
+ Token.Punctuation.OpenParen,
+ Token.Punctuation.String.Begin,
+ Token.Literals.String(
+ 'Object graph should be a Directed Acyclic Graph.'
+ ),
+ Token.Punctuation.String.End,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
+ it('initialization block syntax highlighting matches method body', async () => {
+ const input = Input.InClass(`
+{
+ this.setMessage('test');
+}
+
+public void testMethod() {
+ this.setMessage('test');
+}`);
+ const tokens = await tokenize(input);
+
+ // Find initialization block tokens
+ const initStart = tokens.findIndex((t) => t.text === '{');
+ const initEnd = tokens.findIndex(
+ (t, i) => i > initStart && t.text === '}' && tokens[i - 1]?.text === ';'
+ );
+ const initTokens = tokens.slice(initStart, initEnd + 1);
+
+ // Find method body tokens
+ const methodStart = tokens.findIndex(
+ (t, i) => i > initEnd && tokens[i - 1]?.text === ')' && t.text === '{'
+ );
+ const methodEnd = tokens.findIndex(
+ (t, i) =>
+ i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';'
+ );
+ const methodTokens = tokens.slice(methodStart, methodEnd + 1);
+
+ // Both should have the same highlighting for the string literal
+ const initStringTokens = initTokens.filter(
+ (t) => t.type === 'string.quoted.single.apex'
+ );
+ const methodStringTokens = methodTokens.filter(
+ (t) => t.type === 'string.quoted.single.apex'
+ );
+
+ initStringTokens.length.should.be.greaterThan(0);
+ methodStringTokens.length.should.be.greaterThan(0);
+ initStringTokens[0].type.should.equal(methodStringTokens[0].type);
+ });
+
+ it('static keyword before block is handled (even though static blocks are not valid Apex)', async () => {
+ // Note: Apex does NOT support static initialization blocks like Java
+ // This test verifies the grammar handles the syntax correctly for highlighting
+ // even though it's not valid Apex code
+ const input = Input.InClass(`
+static {
+ Integer x = 5;
+}`);
+ const tokens = await tokenize(input);
+
+ // The static keyword should be matched, then the block should be matched
+ tokens.should.include.deep.members([
+ Token.Keywords.Modifiers.Static,
+ Token.Punctuation.OpenBrace,
+ Token.PrimitiveType.Integer,
+ Token.Identifiers.LocalName('x'),
+ Token.Operators.Assignment,
+ Token.Literals.Numeric.Decimal('5'),
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+ });
+});
diff --git a/test/interface.tests.ts b/test/interface.tests.ts
index aa2cd97..feade0f 100644
--- a/test/interface.tests.ts
+++ b/test/interface.tests.ts
@@ -5,7 +5,7 @@
*--------------------------------------------------------------------------------------------*/
import { should } from 'chai';
-import { tokenize, Token } from './utils/tokenize';
+import { tokenize, Input, Token } from './utils/tokenize';
describe('Grammar', () => {
before(() => {
@@ -78,5 +78,23 @@ interface IBar extends IFoo { }
Token.Punctuation.CloseBrace,
]);
});
+
+ it('interface extends namespace-qualified type (issue #50)', async () => {
+ const input = Input.FromText(
+ `interface MyInterface extends System.IComparable {}`
+ );
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Keywords.Interface,
+ Token.Identifiers.InterfaceName('MyInterface'),
+ Token.Keywords.Extends,
+ Token.Support.Class.System,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.TypeText('IComparable'),
+ Token.Punctuation.OpenBrace,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
});
});
diff --git a/test/repros/.forceignore b/test/repros/.forceignore
new file mode 100644
index 0000000..3e7553c
--- /dev/null
+++ b/test/repros/.forceignore
@@ -0,0 +1,6 @@
+# Standard SFDX ignore patterns
+**/__tests__/**
+**/.sfdx/**
+**/.localdevserver/**
+**/node_modules/**
+
diff --git a/test/repros/.vscode/settings.json b/test/repros/.vscode/settings.json
new file mode 100644
index 0000000..0a40bef
--- /dev/null
+++ b/test/repros/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "xml.preferences.showSchemaDocumentationType": "none"
+}
diff --git a/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls
new file mode 100644
index 0000000..5049a3e
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls
@@ -0,0 +1,25 @@
+/**
+ * PR #68/#69: Annotation on same line as method declaration
+ *
+ * This class demonstrates annotations on the same line as method declarations.
+ * The syntax distinguishes annotations from method modifiers.
+ */
+public class AnnotationOnSameLine_PR68_PR69 {
+
+ @Future(callout=true) public static void futureMethod() {
+ System.debug('Future method');
+ }
+
+ @InvocableMethod(label='Test') public static void invocableMethod() {
+ System.debug('Invocable method');
+ }
+
+ @TestVisible private void testVisibleMethod() {
+ System.debug('Test visible method');
+ }
+
+ @deprecated public void deprecatedMethod() {
+ System.debug('Deprecated method');
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls
new file mode 100644
index 0000000..077e5fc
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls
@@ -0,0 +1,41 @@
+/**
+ * PR #71: DML operations on method call results
+ *
+ * This class demonstrates DML operations performed on method call results.
+ * The syntax highlighting should recognize DML keywords when followed by method calls.
+ */
+public class DMLOnMethodCallResults_PR71 {
+
+ public void dmlOnMapValues() {
+ // Issue #26: DML on Map.values() receives same scope as direct insert
+ Map accounts = new Map();
+ insert accounts.values();
+ insert new List();
+ }
+
+ public void dmlOnQueryResults() {
+ List accounts = [SELECT Id FROM Account];
+ update accounts;
+ delete [SELECT Id FROM Contact];
+ }
+
+ public void dmlOnMethodReturn() {
+ List accounts = getAccounts();
+ upsert accounts;
+ undelete getDeletedAccounts();
+ }
+
+ public void dmlOnChainedMethods() {
+ insert getAccounts().clone();
+ update new Map(getAccounts());
+ }
+
+ private List getAccounts() {
+ return new List();
+ }
+
+ private List getDeletedAccounts() {
+ return new List();
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls
new file mode 100644
index 0000000..4cc8ce4
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls
@@ -0,0 +1,50 @@
+/**
+ * PR #67: Support final keyword in method parameters
+ *
+ * This class demonstrates methods with final keyword in parameters.
+ * The syntax highlighting should properly recognize the final modifier.
+ */
+public class FinalKeywordInMethodParams_PR67 {
+
+ public void methodWithFinalParameter(final String param) {
+ System.debug(param);
+ }
+
+ public void methodWithMultipleFinalParams(final String str, final Integer num) {
+ System.debug(str + num);
+ }
+
+ public void methodWithFinalAndRegular(final String finalParam, String regularParam) {
+ System.debug(finalParam + regularParam);
+ }
+
+ public static void staticMethodWithFinal(final Boolean flag) {
+ System.debug(flag);
+ }
+
+ /**
+ * Test case: final keyword should not unhighlight the type that follows it.
+ * Both types (with and without final) should have consistent syntax highlighting.
+ * Issue: When final precedes a type, the type loses its keyword highlighting.
+ */
+ public void testFinalKeywordDoesNotUnhighlightType(final String str1, String str2) {
+ // str1 parameter: final String - String should be highlighted as keyword
+ // str2 parameter: String - String IS highlighted as keyword (correct)
+ // Both String types should have identical highlighting
+ System.debug(str1 + str2);
+ }
+
+ public void testMultipleTypesWithFinal(final Integer num, final Boolean flag, final Decimal dec) {
+ // All three types (Integer, Boolean, Decimal) should be highlighted as keywords
+ // even though they follow the final keyword
+ System.debug(num + ' ' + flag + ' ' + dec);
+ }
+
+ public void contrastTest(String regularType, final String finalType) {
+ // regularType: String is highlighted (correct)
+ // finalType: String should also be highlighted (but currently isn't)
+ // This demonstrates the highlighting inconsistency
+ System.debug(regularType.equals(finalType));
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls
new file mode 100644
index 0000000..a6699e4
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls
@@ -0,0 +1,82 @@
+/**
+ * PR #73: Initializer block syntax highlighting
+ *
+ * This class demonstrates initialization blocks in nested classes with method calls.
+ * Issue #4920: Initializer block syntax highlighting should match method body highlighting.
+ */
+public class InitializerBlockSyntax_PR73 {
+
+ public class NoneDAGException extends Exception {
+ // Initializer - string literal should be consistently highlighted
+ // Issue: String highlighting is broken in initializer blocks
+ {
+ this.setMessage('Object graph should be a Directed Acyclic Graph.');
+ this.setMessage('Simple string');
+ this.setMessage('String with "quotes" inside');
+ System.debug('Debug message in initializer');
+ }
+
+ // Sample method for comparison - same strings highlighted correctly here
+ public void anotherMethod() {
+ this.setMessage('Object graph should be a Directed Acyclic Graph.');
+ this.setMessage('Simple string');
+ this.setMessage('String with "quotes" inside');
+ System.debug('Debug message in method');
+ }
+ }
+
+ public class ExampleWithInitializer {
+ private String message;
+
+ // Empty initializer
+ {
+ }
+
+ // Initializer with multiple statements
+ {
+ Integer x = 5;
+ String msg = 'test';
+ message = msg;
+ System.debug(message);
+ }
+ }
+
+ public class StaticInitializerExample {
+ private static Integer counter;
+
+ // Note: Apex does NOT support static initialization blocks like Java
+ // This tests grammar handling
+ static {
+ counter = 0;
+ }
+ }
+
+ /**
+ * Test case: String literals in initializer blocks should have consistent highlighting.
+ * Issue: Strings in initializer blocks show inconsistent/incorrect highlighting
+ * compared to the same strings in method bodies.
+ */
+ public class StringHighlightingInInitializer {
+ private String message;
+
+ // Initializer block with various string scenarios
+ {
+ // All these strings should be highlighted consistently
+ message = 'Initializer string';
+ message = 'String with keywords like Object and Graph';
+ message = 'String with numbers 123 and symbols !@#';
+ this.setMessage('Method call with string parameter');
+ System.debug('Debug: initializer block string');
+ }
+
+ // Method body for comparison - same strings work correctly here
+ public void compareMethod() {
+ message = 'Initializer string';
+ message = 'String with keywords like Object and Graph';
+ message = 'String with numbers 123 and symbols !@#';
+ this.setMessage('Method call with string parameter');
+ System.debug('Debug: method body string');
+ }
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls
new file mode 100644
index 0000000..e20035e
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls
@@ -0,0 +1,42 @@
+/**
+ * PR #72: Support namespace-qualified types in extends/implements
+ *
+ * This class demonstrates classes extending and implementing namespace-qualified types.
+ * The syntax highlighting should properly parse System.Exception and Database.Batchable.
+ */
+public class NamespaceQualifiedTypes_PR72 extends System.Exception {
+ // Class extends namespace-qualified type - issue #50
+}
+
+// Another example extending namespace-qualified type
+public class MyCustomException_PR72 extends System.Exception {
+ public MyCustomException_PR72(String message) {
+ super(message);
+ }
+}
+
+// Class implementing namespace-qualified type - issue #50
+public class BatchableProcessor_PR72 implements Database.Batchable {
+ public Database.QueryLocator start(Database.BatchableContext bc) {
+ return Database.getQueryLocator('SELECT Id FROM Account');
+ }
+
+ public void execute(Database.BatchableContext bc, List scope) {
+ // Process accounts
+ Integer foo = 0;
+ }
+
+ public void finish(Database.BatchableContext bc) {
+ // Finish processing
+ }
+}
+
+// Class extending and implementing namespace-qualified types
+public class ComplexExample_PR72 extends System.OctopusException implements Database.Stateful {
+ private Integer state;
+
+ public ComplexExample_PR72(String message) {
+ super(message);
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls
new file mode 100644
index 0000000..f2e81aa
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls
@@ -0,0 +1,138 @@
+/**
+ * PR #74/#75: Switch/when statement syntax highlighting and brace matching
+ *
+ * This class demonstrates switch statements with when clauses.
+ * Issue #2134: Switch/when statement syntax highlighting and brace matching.
+ */
+public class SwitchWhenBraceMatching_PR74_PR75 {
+
+ /**
+ * Test case matching issue #2134:
+ * - Strings in when clause should be consistently syntax highlighted
+ * - Bracket matching should match up pairs
+ * - Brace matching issues when brace follows when clause
+ */
+ public void issue2134_switchWhenBraceMatching() {
+ String param = 'test';
+ switch on param {
+ when 'A' {
+ System.debug('when A');
+ }
+ when 'B' {
+ System.debug('when B');
+ }
+ when 'C'
+ {
+ // Brace on new line after when clause
+ System.debug('when C');
+ }
+ when 'D'{
+ // Brace immediately after when clause without space
+ System.debug('when D');
+ }
+ when else {
+ System.debug('else');
+ }
+ }
+ }
+
+ public void simpleSwitchOnString() {
+ String param = 'A';
+ switch on (param) {
+ when 'A' {
+ System.debug('test');
+ }
+ when else {
+ callExternalMethod();
+ }
+ }
+ }
+
+ public void switchWithMultipleStringValues() {
+ String locale = 'de-CH';
+ switch on locale {
+ when 'de-CH' {
+ System.debug('German Switzerland');
+ }
+ when 'fr-CH' {
+ System.debug('French Switzerland');
+ }
+ when else {
+ System.debug('Other locale');
+ }
+ }
+ }
+
+ public void switchOnInteger() {
+ Integer i = 5;
+ switch on i {
+ when 2, 3, 4 {
+ System.debug('when block 2 and 3 and 4');
+ }
+ when 7 {
+ System.debug('when block 7');
+ }
+ when else {
+ // @TODO.
+ }
+ }
+ }
+
+ public void switchOnSObject() {
+ SObject sobject = new Account();
+ switch on sobject {
+ when Account a {
+ System.debug('account ' + a);
+ }
+ when null {
+ System.debug('null');
+ }
+ when else {
+ System.debug('default');
+ }
+ }
+ }
+
+ public void switchOnMethodResult() {
+ switch on someInteger(getValue()) {
+ when 2, 3, 4 {
+ System.debug('when block 2 and 3 and 4');
+ }
+ when 7 {
+ System.debug('when block 7');
+ }
+ when else {
+ // @TODO.
+ }
+ }
+ }
+
+ public void switchWithSafeNavigator() {
+ MyClass obj = new MyClass();
+ switch on (obj?.param) {
+ when 'A' {
+ System.debug('test');
+ }
+ when else {
+ callExternalMethod();
+ }
+ }
+ }
+
+ private Integer someInteger(Integer val) {
+ return val;
+ }
+
+ private Integer getValue() {
+ return 5;
+ }
+
+ private void callExternalMethod() {
+ System.debug('external');
+ }
+
+ private class MyClass {
+ public String param;
+ }
+}
+
diff --git a/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls
new file mode 100644
index 0000000..702f11a
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls
@@ -0,0 +1,42 @@
+/**
+ * PR #70: Ternary expression syntax highlighting
+ *
+ * This class demonstrates nested ternary expressions and ternary with method calls.
+ * The syntax highlighting should properly distinguish nested conditionals.
+ */
+public class TernaryExpressions_PR70 {
+
+ public void simpleTernary() {
+ Integer result = x ? 19 : 23;
+ }
+
+ public void nestedTernary() {
+ // Nested ternary expression - issue #43
+ Integer result = x ? y ? 1 : 2 : 3;
+ }
+
+ public void ternaryWithMethodCalls() {
+ // Ternary with method call - issue #43
+ String s = condition ? getValue() : getDefault();
+ }
+
+ public void ternaryAsArgument() {
+ processValue(x ? 19 : 23);
+ }
+
+ public void complexNestedTernary() {
+ String output = a ? (b ? 'both' : 'a only') : (c ? 'c only' : 'neither');
+ }
+
+ private String getValue() { return 'value'; }
+ private String getDefault() { return 'default'; }
+ private void processValue(Integer val) { }
+
+ private Boolean x;
+ private Boolean y;
+ private Boolean a;
+ private Boolean b;
+ private Boolean c;
+ private Boolean condition;
+}
+
diff --git a/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml
new file mode 100644
index 0000000..f5e18fd
--- /dev/null
+++ b/test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 60.0
+ Active
+
diff --git a/test/repros/sfdx-project.json b/test/repros/sfdx-project.json
new file mode 100644
index 0000000..46bbf25
--- /dev/null
+++ b/test/repros/sfdx-project.json
@@ -0,0 +1,12 @@
+{
+ "packageDirectories": [
+ {
+ "path": "force-app",
+ "default": true
+ }
+ ],
+ "name": "apex-tmLanguage PR Reproducers",
+ "namespace": "",
+ "sfdcLoginUrl": "https://login.salesforce.com",
+ "sourceApiVersion": "60.0"
+}
\ No newline at end of file
diff --git a/test/switch.tests.ts b/test/switch.tests.ts
index 70afbae..cb2a0a1 100644
--- a/test/switch.tests.ts
+++ b/test/switch.tests.ts
@@ -736,6 +736,73 @@ when 'label' {}
]);
});
+ it('switch with string literals containing dashes', async () => {
+ const input = Input.InMethod(`
+switch on locale {
+when 'de-CH' {
+ System.debug('German Switzerland');
+}
+when 'fr-CH' {
+ System.debug('French Switzerland');
+}
+when else {
+ System.debug('Other locale');
+}
+}`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Keywords.Switch.Switch,
+ Token.Keywords.Switch.On,
+ Token.Variables.ReadWrite('locale'),
+ Token.Punctuation.OpenBrace,
+ Token.Keywords.Switch.When,
+ Token.Punctuation.String.Begin,
+ Token.XmlDocComments.String.SingleQuoted.Text('de-CH'),
+ Token.Punctuation.String.End,
+ Token.Punctuation.OpenBrace,
+ Token.Support.Class.System,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.FunctionText('debug'),
+ Token.Punctuation.OpenParen,
+ Token.XmlDocComments.String.SingleQuoted.Begin,
+ Token.XmlDocComments.String.SingleQuoted.Text('German Switzerland'),
+ Token.XmlDocComments.String.SingleQuoted.End,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ Token.Keywords.Switch.When,
+ Token.Punctuation.String.Begin,
+ Token.XmlDocComments.String.SingleQuoted.Text('fr-CH'),
+ Token.Punctuation.String.End,
+ Token.Punctuation.OpenBrace,
+ Token.Support.Class.System,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.FunctionText('debug'),
+ Token.Punctuation.OpenParen,
+ Token.XmlDocComments.String.SingleQuoted.Begin,
+ Token.XmlDocComments.String.SingleQuoted.Text('French Switzerland'),
+ Token.XmlDocComments.String.SingleQuoted.End,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ Token.Keywords.Switch.When,
+ Token.Keywords.Switch.Else,
+ Token.Punctuation.OpenBrace,
+ Token.Support.Class.System,
+ Token.Punctuation.Accessor,
+ Token.Support.Class.FunctionText('debug'),
+ Token.Punctuation.OpenParen,
+ Token.XmlDocComments.String.SingleQuoted.Begin,
+ Token.XmlDocComments.String.SingleQuoted.Text('Other locale'),
+ Token.XmlDocComments.String.SingleQuoted.End,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Punctuation.CloseBrace,
+ Token.Punctuation.CloseBrace,
+ ]);
+ });
+
/* it('switch usage in triggers', () => {
const input = Input.InTrigger(`
switch on sobject {
diff --git a/test/system.tests.ts b/test/system.tests.ts
index 918b412..ac52840 100644
--- a/test/system.tests.ts
+++ b/test/system.tests.ts
@@ -506,5 +506,51 @@ describe('Grammar', () => {
Token.Punctuation.CloseBrace,
]);
});
+
+ it('DML on Map.values() receives same scope as direct insert (issue #26)', async () => {
+ const input = Input.InMethod(`
+Map accounts = new Map();
+insert accounts.values();
+insert new List();
+`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Type('Map'),
+ Token.Punctuation.TypeParameters.Begin,
+ Token.PrimitiveType.ID,
+ Token.Punctuation.Comma,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Identifiers.LocalName('accounts'),
+ Token.Operators.Assignment,
+ Token.Keywords.Control.New,
+ Token.Type('Map'),
+ Token.Punctuation.TypeParameters.Begin,
+ Token.PrimitiveType.ID,
+ Token.Punctuation.Comma,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Punctuation.OpenParen,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Support.Class.FunctionText('insert'),
+ Token.Variables.Object('accounts'),
+ Token.Punctuation.Accessor,
+ Token.Identifiers.MethodName('values'),
+ Token.Punctuation.OpenParen,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ Token.Support.Class.FunctionText('insert'),
+ Token.Keywords.Control.New,
+ Token.Type('List'),
+ Token.Punctuation.TypeParameters.Begin,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Punctuation.OpenParen,
+ Token.Punctuation.CloseParen,
+ Token.Punctuation.Semicolon,
+ ]);
+ });
});
});
diff --git a/test/type-name.tests.ts b/test/type-name.tests.ts
index c7ab5c9..8d94e7f 100644
--- a/test/type-name.tests.ts
+++ b/test/type-name.tests.ts
@@ -86,5 +86,59 @@ describe('Grammar', () => {
Token.Punctuation.Semicolon,
]);
});
+
+ it('Id type (lowercase d) - Apex is case-insensitive', async () => {
+ const input = Input.InClass(`Id recordId;`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.PrimitiveType.ID,
+ Token.Identifiers.FieldName('recordId'),
+ Token.Punctuation.Semicolon,
+ ]);
+ });
+
+ it('ID type (uppercase D) - Apex is case-insensitive', async () => {
+ const input = Input.InClass(`ID recordId;`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ { text: 'ID', type: 'keyword.type.apex' },
+ Token.Identifiers.FieldName('recordId'),
+ Token.Punctuation.Semicolon,
+ ]);
+ });
+
+ it('Id in generic type parameter', async () => {
+ const input = Input.InClass(`Map accounts;`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Type('Map'),
+ Token.Punctuation.TypeParameters.Begin,
+ Token.PrimitiveType.ID,
+ Token.Punctuation.Comma,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Identifiers.FieldName('accounts'),
+ Token.Punctuation.Semicolon,
+ ]);
+ });
+
+ it('ID in generic type parameter', async () => {
+ const input = Input.InClass(`Map accounts;`);
+ const tokens = await tokenize(input);
+
+ tokens.should.deep.equal([
+ Token.Type('Map'),
+ Token.Punctuation.TypeParameters.Begin,
+ { text: 'ID', type: 'keyword.type.apex' },
+ Token.Punctuation.Comma,
+ Token.Type('Account'),
+ Token.Punctuation.TypeParameters.End,
+ Token.Identifiers.FieldName('accounts'),
+ Token.Punctuation.Semicolon,
+ ]);
+ });
});
});
diff --git a/test/utils/tokenize.ts b/test/utils/tokenize.ts
index 8edd438..81c96fe 100644
--- a/test/utils/tokenize.ts
+++ b/test/utils/tokenize.ts
@@ -646,7 +646,7 @@ export namespace Token {
export const Datetime = createToken('Datetime', 'keyword.type.apex');
export const Decimal = createToken('Decimal', 'keyword.type.apex');
export const Double = createToken('Double', 'keyword.type.apex');
- export const ID = createToken('ID', 'keyword.type.apex');
+ export const ID = createToken('Id', 'keyword.type.apex');
export const Integer = createToken('Integer', 'keyword.type.apex');
export const Long = createToken('Long', 'keyword.type.apex');
export const Object = createToken('Object', 'keyword.type.apex');