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');