From 80f44c4f6fa7e1be30233df6815e273289f73534 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Thu, 30 Oct 2025 12:04:37 -0500 Subject: [PATCH 1/4] fix: support final keyword in method parameters - Update parameter rule to recognize final keyword before type - Add test case for method parameters with final keyword - Fixes issue #49 The parameter rule previously only supported 'this' keyword as an optional modifier. This change extends it to also support 'final', which is commonly used in Apex to prevent parameter reassignment. Closes #49 --- src/apex.tmLanguage.yml | 2 +- test/method.tests.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/apex.tmLanguage.yml b/src/apex.tmLanguage.yml index b50cba4..3cfc1bd 100644 --- a/src/apex.tmLanguage.yml +++ b/src/apex.tmLanguage.yml @@ -1571,7 +1571,7 @@ repository: parameter: match: |- (?x) - (?:(?:\b(this)\b)\s+)? + (?:(?:\b(this|final)\b)\s+)? (? (?: (?:ref\s+)? # ref return diff --git a/test/method.tests.ts b/test/method.tests.ts index e236c19..568a785 100644 --- a/test/method.tests.ts +++ b/test/method.tests.ts @@ -156,6 +156,23 @@ Integer Add(Integer x, Integer y) ]); }); + it('method with final parameter (issue #49)', async () => { + const input = Input.InClass(`void method(final String param) { }`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.Void, + Token.Identifiers.MethodName('method'), + Token.Punctuation.OpenParen, + Token.Keywords.Modifiers.Final, + Token.PrimitiveType.String, + Token.Identifiers.ParameterName('param'), + Token.Punctuation.CloseParen, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); + it('commented parameters are highlighted properly (issue omnisharp-vscode#802)', async () => { const input = Input.InClass( `public void methodWithParametersCommented(Integer p1, /*Integer p2*/, Integer p3) {}` From d1485c196512fb76e8c095d3a68ca85c68ed6bbb Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 14:13:54 -0500 Subject: [PATCH 2/4] test: add test for annotation on same line as method (issue #44) (#68) - Add test case for annotation on same line as method declaration - Test verifies correct highlighting in this scenario - Test currently passes, indicating issue may already be resolved Closes #44 --- test/annotation.tests.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/annotation.tests.ts b/test/annotation.tests.ts index 4e81674..3670bee 100644 --- a/test/annotation.tests.ts +++ b/test/annotation.tests.ts @@ -131,5 +131,27 @@ public class MyTestClass { }`); Token.Punctuation.Semicolon, ]); }); + + it('annotation on same line as method declaration (issue #44)', async () => { + const input = Input.InClass(`@Future(callout=true) public static void method() {}`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.Keywords.Modifiers.AnnotationName('@Future'), + Token.Punctuation.OpenParen, + Token.Variables.ReadWrite('callout'), + Token.Operators.Assignment, + Token.Literals.Boolean.True, + Token.Punctuation.CloseParen, + Token.Keywords.Modifiers.Public, + Token.Keywords.Modifiers.Static, + Token.PrimitiveType.Void, + Token.Identifiers.MethodName('method'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.OpenBrace, + Token.Punctuation.CloseBrace, + ]); + }); }); }); From b6b7737d78ae576aaf394b522acbe2d4fc0052ac Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 14:35:39 -0500 Subject: [PATCH 3/4] test: add tests for ternary expressions (issue #43) (#70) * test: add test for annotation on same line as method (issue #44) - Add test case for annotation on same line as method declaration - Test verifies correct highlighting in this scenario - Test currently passes, indicating issue may already be resolved Closes #44 * test: add tests for ternary expressions (issue #43) - Add test cases for nested ternary expressions - Add test case for ternary with method calls - Tests verify correct highlighting in these scenarios - Tests currently pass, indicating issue may already be resolved Closes #43 --- test/expressions.tests.ts | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/test/expressions.tests.ts b/test/expressions.tests.ts index 7bae3a9..482996e 100644 --- a/test/expressions.tests.ts +++ b/test/expressions.tests.ts @@ -191,6 +191,48 @@ Object newPoint = new Vector(point.x * z, 0);`); Token.Punctuation.Semicolon, ]); }); + + it('nested ternary expression (issue #43)', async () => { + const input = Input.InMethod(`Integer result = x ? y ? 1 : 2 : 3;`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.Integer, + Token.Identifiers.LocalName('result'), + Token.Operators.Assignment, + Token.Variables.ReadWrite('x'), + Token.Operators.Conditional.QuestionMark, + Token.Variables.ReadWrite('y'), + Token.Operators.Conditional.QuestionMark, + Token.Literals.Numeric.Decimal('1'), + Token.Operators.Conditional.Colon, + Token.Literals.Numeric.Decimal('2'), + Token.Operators.Conditional.Colon, + Token.Literals.Numeric.Decimal('3'), + Token.Punctuation.Semicolon, + ]); + }); + + it('ternary with method call (issue #43)', async () => { + const input = Input.InMethod(`String s = x ? getValue() : getDefault();`); + const tokens = await tokenize(input); + + tokens.should.deep.equal([ + Token.PrimitiveType.String, + Token.Identifiers.LocalName('s'), + Token.Operators.Assignment, + Token.Variables.ReadWrite('x'), + Token.Operators.Conditional.QuestionMark, + Token.Identifiers.MethodName('getValue'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Operators.Conditional.Colon, + Token.Identifiers.MethodName('getDefault'), + Token.Punctuation.OpenParen, + Token.Punctuation.CloseParen, + Token.Punctuation.Semicolon, + ]); + }); }); describe('Element Access', () => { From 5efbd5acc69698a24b4b66d6c58b18d51e8ee428 Mon Sep 17 00:00:00 2001 From: Shane McLaughlin Date: Thu, 30 Oct 2025 15:27:51 -0500 Subject: [PATCH 4/4] fix: DML operations on method call results (#26) (#71) * test: add test for annotation on same line as method (issue #44) - Add test case for annotation on same line as method declaration - Test verifies correct highlighting in this scenario - Test currently passes, indicating issue may already be resolved Closes #44 * test: add tests for ternary expressions (issue #43) - Add test cases for nested ternary expressions - Add test case for ternary with method calls - Tests verify correct highlighting in these scenarios - Tests currently pass, indicating issue may already be resolved Closes #43 * fix: DML operations on method call results (issue #26) - Add dml-expression pattern to handle DML operations on expressions - Pattern matches insert/update/delete/upsert/undelete followed by expressions - Ensures DML operations receive same scope (support.function.apex) whether applied to new objects or method call results like Map.values() - Add test case verifying insert accounts.values() gets same scope as insert new List Closes #26 * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to use 'Id' instead of 'ID' to match Apex convention - Add test cases for namespace-qualified extends and implements Closes #50 * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 * test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting * test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. * fix: Fix switch/when statement syntax highlighting and brace matching - Remove lookbehind end patterns from all when-* statements that prevented proper brace matching - Update end patterns to use lookahead that ends on closing brace or next when clause - Improve when-string pattern to support multiple comma-separated strings - Fix issue #2134: syntax highlighting and brace matching for switch/when statements All switch statement tests passing. * fix: Fix dash highlighting in switch statement string literals - Remove quote character from when-statement pattern character class - Change from ['_\-[:alnum:]]+ to [_\-[:alnum:]]+ - Ensures string literals with dashes like 'de-CH' and 'fr-CH' are matched by when-string pattern - Add test case for string literals containing dashes in switch statements This fixes an issue where the when-statement pattern would incorrectly match string literals containing dashes, preventing proper syntax highlighting. * fix: support namespace-qualified types in extends/implements (#50) (#72) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * chore: pr reproducers in sfdx project for visual inspection * fix: 4920 initializer block syntax W-19265631 (#73) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 * test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting * test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. * fix: Fix switch/when statement syntax highlighting and brace matching (issue #2134) (#74) * fix: support namespace-qualified types in extends/implements (issue #50) - Update extends-class pattern to handle namespace-qualified types - Update implements-class pattern to handle namespace-qualified types - Patterns now correctly tokenize System.Exception, Database.Batchable, etc. - Use lookahead to distinguish namespace-qualified from simple types - Fix type-builtin to support both 'Id' and 'ID' (Apex is case-insensitive) - Add test cases for namespace-qualified extends and implements Closes #50 * test: add coverage for both Id and ID (case-insensitive Apex) - Add test for Id (lowercase d) as field type - Add test for ID (uppercase D) as field type - Add test for Id in generic type parameters - Add test for ID in generic type parameters - Verifies Apex case-insensitive support for Id/ID primitive type * feat: support for varied casing on ID/Id * fix: Add syntax highlighting for initialization blocks Add initializer-block pattern to grammar to properly highlight code inside initialization blocks (standalone { } blocks at class member level). The pattern matches standalone curly brace blocks and includes statement patterns for proper syntax highlighting, matching method body behavior. Fixes: https://github.com/forcedotcom/salesforcedx-vscode/issues/4920 * test: Add tests for initialization block syntax highlighting Add comprehensive tests to verify initialization blocks are properly highlighted, including: - Empty initialization blocks - Method calls with string literals (the main issue #4920) - Multiple statements - Nested class scenario - Comparison with method body highlighting * test: Add test for static keyword before block Even though Apex doesn't support static initialization blocks, verify the grammar handles the syntax for highlighting purposes. * fix: Fix switch/when statement syntax highlighting and brace matching - Remove lookbehind end patterns from all when-* statements that prevented proper brace matching - Update end patterns to use lookahead that ends on closing brace or next when clause - Improve when-string pattern to support multiple comma-separated strings - Fix issue #2134: syntax highlighting and brace matching for switch/when statements All switch statement tests passing. * fix: Fix dash highlighting in switch statement string literals - Remove quote character from when-statement pattern character class - Change from ['_\-[:alnum:]]+ to [_\-[:alnum:]]+ - Ensures string literals with dashes like 'de-CH' and 'fr-CH' are matched by when-string pattern - Add test case for string literals containing dashes in switch statements This fixes an issue where the when-statement pattern would incorrectly match string literals containing dashes, preventing proper syntax highlighting. --- .gitignore | 3 + grammars/apex.tmLanguage | 158 ++++++++++++-- grammars/apex.tmLanguage.cson | 102 +++++++-- grammars/soql.tmLanguage | 158 ++++++++++++-- src/apex.tmLanguage.yml | 61 +++++- test/annotation.tests.ts | 6 +- test/class.tests.ts | 37 ++++ test/expressions.tests.ts | 4 +- test/initializer-block.tests.ts | 201 ++++++++++++++++++ test/interface.tests.ts | 20 +- test/repros/.forceignore | 6 + test/repros/.vscode/settings.json | 3 + .../AnnotationOnSameLine_PR68_PR69.cls | 25 +++ ...nnotationOnSameLine_PR68_PR69.cls-meta.xml | 5 + .../classes/DMLOnMethodCallResults_PR71.cls | 41 ++++ .../DMLOnMethodCallResults_PR71.cls-meta.xml | 5 + .../FinalKeywordInMethodParams_PR67.cls | 50 +++++ ...nalKeywordInMethodParams_PR67.cls-meta.xml | 5 + .../classes/InitializerBlockSyntax_PR73.cls | 82 +++++++ .../InitializerBlockSyntax_PR73.cls-meta.xml | 5 + .../classes/NamespaceQualifiedTypes_PR72.cls | 42 ++++ .../NamespaceQualifiedTypes_PR72.cls-meta.xml | 5 + .../SwitchWhenBraceMatching_PR74_PR75.cls | 138 ++++++++++++ ...chWhenBraceMatching_PR74_PR75.cls-meta.xml | 5 + .../classes/TernaryExpressions_PR70.cls | 42 ++++ .../TernaryExpressions_PR70.cls-meta.xml | 5 + test/repros/sfdx-project.json | 12 ++ test/switch.tests.ts | 67 ++++++ test/system.tests.ts | 46 ++++ test/type-name.tests.ts | 54 +++++ test/utils/tokenize.ts | 2 +- 31 files changed, 1334 insertions(+), 61 deletions(-) create mode 100644 test/initializer-block.tests.ts create mode 100644 test/repros/.forceignore create mode 100644 test/repros/.vscode/settings.json create mode 100644 test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls create mode 100644 test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls create mode 100644 test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls create mode 100644 test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls create mode 100644 test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls create mode 100644 test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls create mode 100644 test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml create mode 100644 test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls create mode 100644 test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml create mode 100644 test/repros/sfdx-project.json 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');