Skip to content

Commit d1695dd

Browse files
authored
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: forcedotcom/salesforcedx-vscode#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.
1 parent f2f3ff2 commit d1695dd

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

grammars/apex.tmLanguage

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,10 @@
175175
<key>include</key>
176176
<string>#method-declaration</string>
177177
</dict>
178+
<dict>
179+
<key>include</key>
180+
<string>#initializer-block</string>
181+
</dict>
178182
<dict>
179183
<key>include</key>
180184
<string>#punctuation-semicolon</string>
@@ -2514,6 +2518,36 @@
25142518
</dict>
25152519
</array>
25162520
</dict>
2521+
<key>initializer-block</key>
2522+
<dict>
2523+
<key>begin</key>
2524+
<string>\{</string>
2525+
<key>beginCaptures</key>
2526+
<dict>
2527+
<key>0</key>
2528+
<dict>
2529+
<key>name</key>
2530+
<string>punctuation.curlybrace.open.apex</string>
2531+
</dict>
2532+
</dict>
2533+
<key>end</key>
2534+
<string>\}</string>
2535+
<key>endCaptures</key>
2536+
<dict>
2537+
<key>0</key>
2538+
<dict>
2539+
<key>name</key>
2540+
<string>punctuation.curlybrace.close.apex</string>
2541+
</dict>
2542+
</dict>
2543+
<key>patterns</key>
2544+
<array>
2545+
<dict>
2546+
<key>include</key>
2547+
<string>#statement</string>
2548+
</dict>
2549+
</array>
2550+
</dict>
25172551
<key>variable-initializer</key>
25182552
<dict>
25192553
<key>begin</key>

grammars/apex.tmLanguage.cson

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ repository:
119119
{
120120
include: '#method-declaration'
121121
}
122+
{
123+
include: '#initializer-block'
124+
}
122125
{
123126
include: '#punctuation-semicolon'
124127
}
@@ -1504,6 +1507,20 @@ repository:
15041507
include: '#statement'
15051508
}
15061509
]
1510+
'initializer-block':
1511+
begin: '\\{'
1512+
beginCaptures:
1513+
'0':
1514+
name: 'punctuation.curlybrace.open.apex'
1515+
end: '\\}'
1516+
endCaptures:
1517+
'0':
1518+
name: 'punctuation.curlybrace.close.apex'
1519+
patterns: [
1520+
{
1521+
include: '#statement'
1522+
}
1523+
]
15071524
'variable-initializer':
15081525
begin: '(?<!=|!)(=)(?!=|>)'
15091526
beginCaptures:

grammars/soql.tmLanguage

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@
161161
<key>include</key>
162162
<string>#method-declaration</string>
163163
</dict>
164+
<dict>
165+
<key>include</key>
166+
<string>#initializer-block</string>
167+
</dict>
164168
<dict>
165169
<key>include</key>
166170
<string>#punctuation-semicolon</string>
@@ -2496,6 +2500,36 @@
24962500
</dict>
24972501
</array>
24982502
</dict>
2503+
<key>initializer-block</key>
2504+
<dict>
2505+
<key>begin</key>
2506+
<string>\{</string>
2507+
<key>beginCaptures</key>
2508+
<dict>
2509+
<key>0</key>
2510+
<dict>
2511+
<key>name</key>
2512+
<string>punctuation.curlybrace.open.apex</string>
2513+
</dict>
2514+
</dict>
2515+
<key>end</key>
2516+
<string>\}</string>
2517+
<key>endCaptures</key>
2518+
<dict>
2519+
<key>0</key>
2520+
<dict>
2521+
<key>name</key>
2522+
<string>punctuation.curlybrace.close.apex</string>
2523+
</dict>
2524+
</dict>
2525+
<key>patterns</key>
2526+
<array>
2527+
<dict>
2528+
<key>include</key>
2529+
<string>#statement</string>
2530+
</dict>
2531+
</array>
2532+
</dict>
24992533
<key>variable-initializer</key>
25002534
<dict>
25012535
<key>begin</key>

src/apex.tmLanguage.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ repository:
5454
- include: '#variable-initializer'
5555
- include: '#constructor-declaration'
5656
- include: '#method-declaration'
57+
- include: '#initializer-block'
5758
- include: '#punctuation-semicolon'
5859

5960
interface-members:
@@ -859,6 +860,16 @@ repository:
859860
patterns:
860861
- include: '#statement'
861862

863+
initializer-block:
864+
begin: \{
865+
beginCaptures:
866+
'0': { name: punctuation.curlybrace.open.apex }
867+
end: \}
868+
endCaptures:
869+
'0': { name: punctuation.curlybrace.close.apex }
870+
patterns:
871+
- include: '#statement'
872+
862873
variable-initializer:
863874
begin: (?<!=|!)(=)(?!=|>)
864875
beginCaptures:

test/initializer-block.tests.ts

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Modifications Copyright (c) 2018 Salesforce.
4+
* See LICENSE in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
import { should } from 'chai';
8+
import { tokenize, Input, Token } from './utils/tokenize';
9+
10+
describe('Grammar', () => {
11+
before(() => {
12+
should();
13+
});
14+
15+
describe('Initializer Blocks', () => {
16+
it('empty initialization block', async () => {
17+
const input = Input.InClass(`{ }`);
18+
const tokens = await tokenize(input);
19+
20+
tokens.should.deep.equal([
21+
Token.Punctuation.OpenBrace,
22+
Token.Punctuation.CloseBrace,
23+
]);
24+
});
25+
26+
it('initialization block with method call and string literal', async () => {
27+
const input = Input.InClass(`
28+
{
29+
this.setMessage('Object graph should be a Directed Acyclic Graph.');
30+
}`);
31+
const tokens = await tokenize(input);
32+
33+
tokens.should.deep.equal([
34+
Token.Punctuation.OpenBrace,
35+
Token.Keywords.This,
36+
Token.Punctuation.Accessor,
37+
Token.Identifiers.MethodName('setMessage'),
38+
Token.Punctuation.OpenParen,
39+
Token.Punctuation.String.Begin,
40+
Token.Literals.String(
41+
'Object graph should be a Directed Acyclic Graph.'
42+
),
43+
Token.Punctuation.String.End,
44+
Token.Punctuation.CloseParen,
45+
Token.Punctuation.Semicolon,
46+
Token.Punctuation.CloseBrace,
47+
]);
48+
});
49+
50+
it('initialization block with multiple statements', async () => {
51+
const input = Input.InClass(`
52+
{
53+
Integer x = 5;
54+
String message = 'test';
55+
this.setMessage(message);
56+
}`);
57+
const tokens = await tokenize(input);
58+
59+
tokens.should.deep.equal([
60+
Token.Punctuation.OpenBrace,
61+
Token.PrimitiveType.Integer,
62+
Token.Identifiers.LocalName('x'),
63+
Token.Operators.Assignment,
64+
Token.Literals.Numeric.Decimal('5'),
65+
Token.Punctuation.Semicolon,
66+
Token.PrimitiveType.String,
67+
Token.Identifiers.LocalName('message'),
68+
Token.Operators.Assignment,
69+
Token.Punctuation.String.Begin,
70+
Token.Literals.String('test'),
71+
Token.Punctuation.String.End,
72+
Token.Punctuation.Semicolon,
73+
Token.Keywords.This,
74+
Token.Punctuation.Accessor,
75+
Token.Identifiers.MethodName('setMessage'),
76+
Token.Punctuation.OpenParen,
77+
Token.Variables.ReadWrite('message'),
78+
Token.Punctuation.CloseParen,
79+
Token.Punctuation.Semicolon,
80+
Token.Punctuation.CloseBrace,
81+
]);
82+
});
83+
84+
it('initialization block in nested class (issue #4920)', async () => {
85+
const input = Input.FromText(`
86+
public class TestDataBuilder {
87+
public class NoneDAGException extends Exception {
88+
// Initializer
89+
{
90+
this.setMessage('Object graph should be a Directed Acyclic Graph.');
91+
}
92+
93+
// Sample method for comparison
94+
public void anotherMethod() {
95+
this.setMessage('Object graph should be a Directed Acyclic Graph.');
96+
}
97+
}
98+
}`);
99+
const tokens = await tokenize(input);
100+
101+
// Find the initialization block tokens (should start after the comment)
102+
const initBlockStart = tokens.findIndex(
103+
(t, i) =>
104+
i > 0 &&
105+
tokens[i - 1].text === '//' &&
106+
tokens[i].text === 'Initializer'
107+
);
108+
const initBlockEnd = tokens.findIndex(
109+
(t, i) =>
110+
i > initBlockStart && t.text === '}' && tokens[i - 1]?.text === ';'
111+
);
112+
113+
// Extract tokens for the initialization block
114+
const initBlockTokens = tokens.slice(
115+
initBlockStart + 3, // Skip comment tokens
116+
initBlockEnd + 1
117+
);
118+
119+
// Verify initialization block has proper string highlighting
120+
initBlockTokens.should.include.deep.members([
121+
Token.Punctuation.OpenBrace,
122+
Token.Keywords.This,
123+
Token.Punctuation.Accessor,
124+
Token.Identifiers.MethodName('setMessage'),
125+
Token.Punctuation.OpenParen,
126+
Token.Punctuation.String.Begin,
127+
Token.Literals.String(
128+
'Object graph should be a Directed Acyclic Graph.'
129+
),
130+
Token.Punctuation.String.End,
131+
Token.Punctuation.CloseParen,
132+
Token.Punctuation.Semicolon,
133+
Token.Punctuation.CloseBrace,
134+
]);
135+
});
136+
137+
it('initialization block syntax highlighting matches method body', async () => {
138+
const input = Input.InClass(`
139+
{
140+
this.setMessage('test');
141+
}
142+
143+
public void testMethod() {
144+
this.setMessage('test');
145+
}`);
146+
const tokens = await tokenize(input);
147+
148+
// Find initialization block tokens
149+
const initStart = tokens.findIndex((t) => t.text === '{');
150+
const initEnd = tokens.findIndex(
151+
(t, i) => i > initStart && t.text === '}' && tokens[i - 1]?.text === ';'
152+
);
153+
const initTokens = tokens.slice(initStart, initEnd + 1);
154+
155+
// Find method body tokens
156+
const methodStart = tokens.findIndex(
157+
(t, i) => i > initEnd && tokens[i - 1]?.text === ')' && t.text === '{'
158+
);
159+
const methodEnd = tokens.findIndex(
160+
(t, i) =>
161+
i > methodStart && t.text === '}' && tokens[i - 1]?.text === ';'
162+
);
163+
const methodTokens = tokens.slice(methodStart, methodEnd + 1);
164+
165+
// Both should have the same highlighting for the string literal
166+
const initStringTokens = initTokens.filter(
167+
(t) => t.type === 'string.quoted.single.apex'
168+
);
169+
const methodStringTokens = methodTokens.filter(
170+
(t) => t.type === 'string.quoted.single.apex'
171+
);
172+
173+
initStringTokens.length.should.be.greaterThan(0);
174+
methodStringTokens.length.should.be.greaterThan(0);
175+
initStringTokens[0].type.should.equal(methodStringTokens[0].type);
176+
});
177+
178+
it('static keyword before block is handled (even though static blocks are not valid Apex)', async () => {
179+
// Note: Apex does NOT support static initialization blocks like Java
180+
// This test verifies the grammar handles the syntax correctly for highlighting
181+
// even though it's not valid Apex code
182+
const input = Input.InClass(`
183+
static {
184+
Integer x = 5;
185+
}`);
186+
const tokens = await tokenize(input);
187+
188+
// The static keyword should be matched, then the block should be matched
189+
tokens.should.include.deep.members([
190+
Token.Keywords.Modifiers.Static,
191+
Token.Punctuation.OpenBrace,
192+
Token.PrimitiveType.Integer,
193+
Token.Identifiers.LocalName('x'),
194+
Token.Operators.Assignment,
195+
Token.Literals.Numeric.Decimal('5'),
196+
Token.Punctuation.Semicolon,
197+
Token.Punctuation.CloseBrace,
198+
]);
199+
});
200+
});
201+
});

0 commit comments

Comments
 (0)