Skip to content

Commit

Permalink
Add Apex lexer (#1103)
Browse files Browse the repository at this point in the history
This commit adds a lexer for the Apex language.
  • Loading branch information
malukenho authored and pyrmont committed Sep 17, 2019
1 parent 3398f6f commit a09305e
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 1 deletion.
9 changes: 9 additions & 0 deletions lib/rouge/demos/apex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
public class with sharing Trigger {
@Deprecated
public void resolveSum(int x, int y) {
System.debug('x is ' + x);
System.debug('y is ' + y);

System.debug('x + y = ' + (x+y));
}
}
5 changes: 5 additions & 0 deletions lib/rouge/guessers/disambiguation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ def match?(filename)

next PlainText
end

disambiguate '*.cls' do
next TeX if matches?(/\A\s*(?:\\|%)/)
next Apex
end
end
end
end
126 changes: 126 additions & 0 deletions lib/rouge/lexers/apex.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

module Rouge
module Lexers
class Apex < RegexLexer
title "Apex"
desc "The Apex programming language (provided by salesforce)"

tag 'apex'
filenames '*.cls'
mimetypes 'text/x-apex'

def self.keywords
@keywords ||= Set.new %w(
assert break case catch continue default do else finally for if goto
instanceof new return switch this throw try while insert update
delete
)
end

def self.declarations
@declarations ||= Set.new %w(
abstract const enum extends final implements native private protected
public static super synchronized throws transient volatile with
sharing without inherited virtual global testmethod
)
end

def self.soql
@soql ||= Set.new %w(
SELECT FROM WHERE UPDATE LIKE TYPEOF END USING SCOPE WITH DATA
CATEGORY GROUP BY ROLLUP CUBE HAVING ORDER BY ASC DESC NULLS FIRST
LAST LIMIT OFFSET FOR VIEW REFERENCE UPDATE TRACKING VIEWSTAT OR AND
)
end

def self.types
@types ||= Set.new %w(
String boolean byte char double float int long short var void
)
end

def self.constants
@constants ||= Set.new %w(true false null)
end

id = /[a-z_][a-z0-9_]*/i

state :root do
rule %r/\s+/m, Text

rule %r(//.*?$), Comment::Single
rule %r(/\*.*?\*/)m, Comment::Multiline

rule %r/(?:class|interface)\b/, Keyword::Declaration, :class
rule %r/import\b/, Keyword::Namespace, :import

rule %r/([@$.]?)(#{id})([:(]?)/io do |m|
if self.class.keywords.include? m[0].downcase
token Keyword
elsif self.class.soql.include? m[0].upcase
token Keyword
elsif self.class.declarations.include? m[0].downcase
token Keyword::Declaration
elsif self.class.types.include? m[0].downcase
token Keyword::Type
elsif self.class.constants.include? m[0].downcase
token Keyword::Constant
elsif 'package'.casecmp m[0]
token Keyword::Namespace
elsif m[1] == "@"
token Name::Decorator
elsif m[3] == ":"
groups Operator, Name::Label, Punctuation
elsif m[3] == "("
groups Operator, Name::Function, Punctuation
elsif m[1] == "."
groups Operator, Name::Property, Punctuation
else
token Name
end
end

rule %r/"/, Str::Double, :dq
rule %r/'/, Str::Single, :sq

digit = /[0-9]_+[0-9]|[0-9]/
rule %r/#{digit}+\.#{digit}+([eE]#{digit}+)?[fd]?/, Num::Float
rule %r/0b(?:[01]_+[01]|[01])+/i, Num::Bin
rule %r/0x(?:\h_+\h|\h)+/i, Num::Hex
rule %r/0(?:[0-7]_+[0-7]|[0-7])+/, Num::Oct
rule %r/#{digit}+L?/, Num::Integer

rule %r/[-+\/*~^!%&<>|=.?]/, Operator
rule %r/[\[\](){},:;]/, Punctuation;
end

state :class do
rule %r/\s+/m, Text
rule id, Name::Class, :pop!
end

state :import do
rule %r/\s+/m, Text
rule %r/[a-z0-9_.]+\*?/i, Name::Namespace, :pop!
end

state :escape do
rule %r/\\[btnfr\\"']/, Str::Escape
end

state :dq do
mixin :escape
rule %r/[^\\"]+/, Str::Double
rule %r/"/, Str::Double, :pop!
end

state :sq do
mixin :escape
rule %r/[^\\']+/, Str::Double
rule %r/'/, Str::Double, :pop!
end
end
end
end
18 changes: 18 additions & 0 deletions spec/lexers/apex_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*- #
# frozen_string_literal: true

describe Rouge::Lexers::Apex do
let(:subject) { Rouge::Lexers::Apex.new }

describe 'guessing' do
include Support::Guessing

it 'guesses by filename' do
assert_guess :filename => 'foo.cls', :source => '// A comment'
end

it 'guesses by mimetype' do
assert_guess :mimetype => 'text/x-apex'
end
end
end
2 changes: 1 addition & 1 deletion spec/lexers/tex_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
assert_guess :filename => 'foo.toc'
assert_guess :filename => 'foo.aux'
assert_guess :filename => 'foo.sty'
assert_guess :filename => 'foo.cls'
assert_guess :filename => 'foo.cls', :source => '\\documentclass'
end

it 'guesses by mimetype' do
Expand Down
177 changes: 177 additions & 0 deletions spec/visual/samples/apex
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
////// badcase.apex //////

if (Reports.get(0) instanceof CustomReport) {
// Can safely cast it back to a custom report object
CustomReport c = (CustomReport) Reports.get(0);
} else {
// Do something with the non-custom-report.
}

Object o = null;
Boolean result = o instanceof Account;
System.assertEquals(false, result);

String dq = "This is a double-quoted string.";
String sq = 'This is a single-quoted string.';
String es_dq = "This is a string with \" escape sequences \b.";
String es_sq = 'This is a string with \' escape sequences \n.';

1;
1.0;
0b01;
0xFFFFFF;
01;

public virtual class SuperClass {
public String mySalutation;
public String myFirstName;
public String myLastName;

public SuperClass() {

mySalutation = 'Mr.';
myFirstName = 'Carl';
myLastName = 'Vonderburg';
}

public SuperClass(String salutation, String firstName, String lastName) {

mySalutation = salutation;
myFirstName = firstName;
myLastName = lastName;
}

public virtual void printName() {

System.debug('My name is ' + mySalutation + myLastName);
}

public virtual String getFirstName() {
return myFirstName;
}
}

public class Subclass extends Superclass {
public override void printName() {
super.printName();
System.debug('But you can call me ' + super.getFirstName());
}
}

public Subclass() {
super('Madam', 'Brenda', 'Clapentrap');
}

// using `this`
public class myTestThis {

string s;
{
this.s = 'TestString';
}
}

public class testThis {

// First constructor for the class. It requires a string parameter.
public testThis(string s2) {
}

// Second constructor for the class. It does not require a parameter.
// This constructor calls the first constructor using the this keyword.
public testThis() {
this('None');
}
}

// using transient
public class ExampleController {

DateTime t1;
transient DateTime t2;

public String getT1() {
if (t1 == null) t1 = System.now();
return '' + t1;
}

public String getT2() {
if (t2 == null) t2 = System.now();
return '' + t2;
}
}

// page controller
public inherited sharing class InheritedSharingClass{
public List<Contact> getAllTheSecrets(){
return [SELECT Name FROM Contact];
}
}

// annotations
global class MyClass {

@Future(callout=true) @IsTest
public static void myMethod(String a)
{
/* ... */
}
}

// Invokables
public without sharing class AccountQueryAction {
@InvocableMethod(label='Get Account Names' description='Returns the list of account names corresponding to the specified account IDs.')
public static List<String> getAccountNames(List<ID> ids) {
List<String> accountNames = new List<String>();
List<Account> accounts = [SELECT Name FROM Account WHERE Id in :ids];
for (Account account : accounts) {
accountNames.add(account.Name);
}
return accountNames;
}
}

public class AccountInsertAction {
@InvocableMethod(label='Insert Accounts' description='Inserts the accounts specified and returns the IDs of the new accounts.')
public static List<ID> insertAccounts(List<Account> accounts) {
Database.SaveResult[] results = Database.insert(accounts);
List<ID> accountIds = new List<ID>();
for (Database.SaveResult result : results) {
if (result.isSuccess()) {
accountIds.add(result.getId());
}
}
return accountIds;
}
}

// test class
@isTest(SeeAllData=true)
public class TestDataAccessClass {

static testmethod void myTestMethod1() {
// Query an existing account in the organization.
Account a = [SELECT Id, Name FROM Account WHERE Name='Acme' LIMIT 1];
System.assert(a != null);

// Create a test account based on the queried account.
Account testAccount = a.clone();
testAccount.Name = 'Acme Test';

insert testAccount;

// Query the test account that was inserted.
Account testAccount2 = [SELECT Id, Name FROM Account
WHERE Name='Acme Test' LIMIT 1];

System.assert(testAccount2 != null);
}

@isTest static void myTestMethod2() {
// Can access all data in the organization.
}

}

// Soql
[SELECT Company, toLabel(Status) FROM Lead WHERE toLabel(Status) = 'le Draft']

0 comments on commit a09305e

Please sign in to comment.