├── .prettierrc.json ├── test ├── soql │ ├── snapshots │ │ ├── example-COUNT.soql │ │ ├── simple_account.soql │ │ ├── example-simple-query.soql │ │ ├── example-LIMIT.soql │ │ ├── example-ORDERY_BY.soql │ │ ├── example_GROUP_BY.soql │ │ ├── example-OFFSET_ORDER_BY.soql │ │ ├── example-HAVING.soql │ │ ├── example-WHERE.soql │ │ ├── example-OFFSET_ORDER_BY_LIMIT.soql │ │ ├── example-ORDER_BY_LIMIT.soql │ │ ├── example-parent-to-child_CUSTOM_OBJECTS.soql │ │ ├── example-relationship-WHERE.soql │ │ ├── example-child-to-parent-CUSTOM_OBJECTS.soql │ │ ├── example-child-to-parent.soql │ │ ├── example-relationship_TYPEOF.soql │ │ ├── example-parent-to-child.soql │ │ ├── example-relationship-timerange.soql │ │ ├── some_functions.soql │ │ ├── example-relationship-polymorphic-key.soql │ │ ├── example-relationship-with-aggregate.soql │ │ ├── example-COUNT.soql.snap │ │ ├── simple_account.soql.snap │ │ ├── example-simple-query.soql.snap │ │ ├── example-ORDERY_BY.soql.snap │ │ ├── example_GROUP_BY.soql.snap │ │ ├── example-OFFSET_ORDER_BY.soql.snap │ │ ├── example-LIMIT.soql.snap │ │ ├── example-OFFSET_ORDER_BY_LIMIT.soql.snap │ │ ├── example-WHERE.soql.snap │ │ ├── example-HAVING.soql.snap │ │ ├── example-child-to-parent-CUSTOM_OBJECTS.soql.snap │ │ ├── example-parent-to-child_CUSTOM_OBJECTS.soql.snap │ │ ├── example-ORDER_BY_LIMIT.soql.snap │ │ ├── example-child-to-parent.soql.snap │ │ ├── some_functions.soql.snap │ │ ├── example-parent-to-child.soql.snap │ │ ├── example-relationship-timerange.soql.snap │ │ ├── example-relationship-WHERE.soql.snap │ │ ├── example-relationship_TYPEOF.soql.snap │ │ ├── example-relationship-polymorphic-key.soql.snap │ │ └── example-relationship-with-aggregate.soql.snap │ ├── highlight_partial_from.soql │ ├── simple_account.soql │ ├── header-comments.soql │ └── timestamp_literals.soql ├── repros │ ├── .vscode │ │ └── settings.json │ ├── .forceignore │ ├── force-app │ │ └── main │ │ │ └── default │ │ │ └── classes │ │ │ ├── TernaryExpressions_PR70.cls-meta.xml │ │ │ ├── DMLOnMethodCallResults_PR71.cls-meta.xml │ │ │ ├── InitializerBlockSyntax_PR73.cls-meta.xml │ │ │ ├── NamespaceQualifiedTypes_PR72.cls-meta.xml │ │ │ ├── AnnotationOnSameLine_PR68_PR69.cls-meta.xml │ │ │ ├── FinalKeywordInMethodParams_PR67.cls-meta.xml │ │ │ ├── SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml │ │ │ ├── TernaryWithDecimals_W8095488.cls-meta.xml │ │ │ ├── AnnotationOnSameLine_PR68_PR69.cls │ │ │ ├── TernaryExpressions_PR70.cls │ │ │ ├── DMLOnMethodCallResults_PR71.cls │ │ │ ├── NamespaceQualifiedTypes_PR72.cls │ │ │ ├── FinalKeywordInMethodParams_PR67.cls │ │ │ ├── TernaryWithDecimals_W8095488.cls │ │ │ ├── InitializerBlockSyntax_PR73.cls │ │ │ └── SwitchWhenBraceMatching_PR74_PR75.cls │ └── sfdx-project.json ├── incomplete-code.test.ts ├── utils │ └── registry.ts ├── local.test.ts ├── interface.test.ts ├── type-name.test.ts ├── javadoc.test.ts ├── field.test.ts ├── annotation.test.ts ├── literals.test.ts ├── enum.test.ts ├── constructor.test.ts ├── initializer-block.test.ts ├── xml-doc-comment.test.ts ├── class.test.ts ├── for-statements.test.ts └── trigger.test.ts ├── .prettierignore ├── .commitlintrc.json ├── docs ├── publishing.md ├── coding-guidelines.md └── commit-guidelines.md ├── .vscode ├── tasks.json ├── settings.json └── extensions.json ├── CODEOWNERS ├── .github └── workflows │ ├── testCommitExceptMain.yml │ ├── onPushToMain.yml │ ├── validatePR.yml │ ├── onRelease.yml │ ├── slackNotify.yml │ ├── automerge.yml │ └── manualRelease.yml ├── .npmignore ├── .editorconfig ├── ISSUE_TEMPLATE ├── tsconfig.json ├── SECURITY.md ├── .gitignore ├── src ├── soql.tmLanguage.template.yml └── syntax.md ├── scripts ├── build-grammars.js ├── build-atom.js └── build-soql.js ├── LICENSE ├── package.json ├── README.md └── CODE_OF_CONDUCT.md /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-COUNT.soql: -------------------------------------------------------------------------------- 1 | SELECT COUNT() FROM Contact 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | // .prettierignore 2 | **/*.yml 3 | node_modules 4 | out 5 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /test/soql/snapshots/simple_account.soql: -------------------------------------------------------------------------------- 1 | 2 | SELECT Id, Name FROM Account 3 | 4 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-simple-query.soql: -------------------------------------------------------------------------------- 1 | SELECT Id, Name, BillingCity FROM Account 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-LIMIT.soql: -------------------------------------------------------------------------------- 1 | SELECT Name FROM Account WHERE Industry = 'media' LIMIT 125 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-ORDERY_BY.soql: -------------------------------------------------------------------------------- 1 | SELECT Name FROM Account ORDER BY Name DESC NULLS LAST 2 | -------------------------------------------------------------------------------- /test/repros/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "xml.preferences.showSchemaDocumentationType": "none" 3 | } 4 | -------------------------------------------------------------------------------- /test/soql/snapshots/example_GROUP_BY.soql: -------------------------------------------------------------------------------- 1 | SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-OFFSET_ORDER_BY.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, Id FROM Merchandise__c ORDER BY Name OFFSET 100 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-HAVING.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-WHERE.soql: -------------------------------------------------------------------------------- 1 | SELECT Id FROM Contact WHERE Name LIKE 'A%' AND MailingCity = 'California' 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-OFFSET_ORDER_BY_LIMIT.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, Id FROM Merchandise__c ORDER BY Name LIMIT 20 OFFSET 100 2 | -------------------------------------------------------------------------------- /docs/publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing 2 | 3 | Publishing happens automatically when a commit with the tag `feat` or `fix` is merged to main. 4 | -------------------------------------------------------------------------------- /test/repros/.forceignore: -------------------------------------------------------------------------------- 1 | # Standard SFDX ignore patterns 2 | **/__tests__/** 3 | **/.sfdx/** 4 | **/.localdevserver/** 5 | **/node_modules/** 6 | 7 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-ORDER_BY_LIMIT.soql: -------------------------------------------------------------------------------- 1 | SELECT Name FROM Account WHERE Industry = 'media' ORDER BY BillingPostalCode ASC NULLS LAST LIMIT 125 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-parent-to-child_CUSTOM_OBJECTS.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, (SELECT Name FROM Line_Items__r) FROM Merchandise__c WHERE Name LIKE ‘Acme%’ 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-WHERE.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, (SELECT LastName FROM Contacts WHERE CreatedBy.Alias = 'x') FROM Account WHERE Industry = 'media' 2 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0" 5 | } 6 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-child-to-parent-CUSTOM_OBJECTS.soql: -------------------------------------------------------------------------------- 1 | SELECT Id, FirstName__c, Mother_of_Child__r.FirstName__c FROM Daughter__c WHERE Mother_of_Child__r.LastName__c LIKE 'C%' 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-child-to-parent.soql: -------------------------------------------------------------------------------- 1 | SELECT Contact.FirstName, Contact.Account.Name FROM Contact; 2 | SELECT Id, Name, Account.Name FROM Contact WHERE Account.Industry = 'media' 3 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship_TYPEOF.soql: -------------------------------------------------------------------------------- 1 | SELECT TYPEOF What WHEN Account THEN Phone, NumberOfEmployees WHEN Opportunity THEN Amount, CloseDate ELSE Name, Email END FROM Event 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-parent-to-child.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, (SELECT LastName FROM Contacts) FROM Account; 2 | 3 | SELECT Account.Name, (SELECT Contact.LastName FROM Account.Contacts) FROM Account 4 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-timerange.soql: -------------------------------------------------------------------------------- 1 | SELECT UserId, COUNT(Id) FROM LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000Z AND LoginTime < 2010-09-21T22:16:30.000Z GROUP BY UserId 2 | -------------------------------------------------------------------------------- /test/soql/snapshots/some_functions.soql: -------------------------------------------------------------------------------- 1 | 2 | SELECT city_c, State_c, COUNT(Employee_Name__C) Counts, COUNT_DISTINCT(Employee_Name__C) DistCounts 3 | FROM Employee__C 4 | GROUP BY ROLLUP (City__c, State__C) 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | #GUSINFO: Platform Dev Tools Scrum Team, Platform Dev Tools 4 | * 5 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-polymorphic-key.soql: -------------------------------------------------------------------------------- 1 | SELECT Id, Owner.Name FROM Task WHERE Owner.FirstName like 'B%'; 2 | 3 | SELECT Id, Who.FirstName, Who.LastName FROM Task WHERE Owner.FirstName LIKE 'B%'; 4 | 5 | SELECT Id, What.Name FROM Event; 6 | -------------------------------------------------------------------------------- /.github/workflows/testCommitExceptMain.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches-ignore: [main] 6 | 7 | jobs: 8 | unit-tests: 9 | uses: salesforcecli/github-workflows/.github/workflows/unitTest.yml@main 10 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Dev config folders 2 | /.vscode 3 | /docs 4 | /src 5 | /test 6 | /scripts 7 | 8 | # Configuration files 9 | .editorconfig 10 | .prettierignore 11 | .prettierrc.json 12 | gulpfile.js 13 | ISSUE_TEMPLATE 14 | tsconfig.json 15 | 16 | # Ignore artifacts 17 | *.tgz -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 60.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/TernaryWithDecimals_W8095488.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 58.0 4 | Active 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-with-aggregate.soql: -------------------------------------------------------------------------------- 1 | SELECT Name, (SELECT CreatedBy.Name FROM Notes) FROM Account; 2 | 3 | SELECT Amount, Id, Name, (SELECT Quantity, ListPrice, PricebookEntry.UnitPrice, PricebookEntry.Name FROM OpportunityLineItems) FROM Opportunity 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [{.travis.yml},package.json] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | [*.json] 11 | indent_style = space 12 | indent_size = 2 -------------------------------------------------------------------------------- /test/repros/sfdx-project.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageDirectories": [ 3 | { 4 | "path": "force-app", 5 | "default": true 6 | } 7 | ], 8 | "name": "apex-tmLanguage PR Reproducers", 9 | "namespace": "", 10 | "sfdcLoginUrl": "https://login.salesforce.com", 11 | "sourceApiVersion": "60.0" 12 | } -------------------------------------------------------------------------------- /ISSUE_TEMPLATE: -------------------------------------------------------------------------------- 1 | ## Details 2 | 3 | What editor are you seeing the problem in? (e.g. Atom, Visual Studio Code, etc.) 4 | 5 | What version of the editor are you using? 6 | 7 | What color theme are you using? 8 | 9 | ## Repro 10 | 11 | Please provide a code example and (optionally) a screenshot demonstrating the problem. -------------------------------------------------------------------------------- /.github/workflows/onPushToMain.yml: -------------------------------------------------------------------------------- 1 | name: Version, Tag and Github Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | release: 9 | uses: salesforcecli/github-workflows/.github/workflows/githubRelease.yml@gbockus/updateReleaseToIncludeTokenInput 10 | secrets: 11 | ALT_GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": ["es6", "dom"], 6 | "sourceMap": true, 7 | "declaration": true, 8 | "moduleResolution": "node", 9 | "noImplicitAny": false, 10 | "outDir": "./out/test", 11 | "preserveConstEnums": true, 12 | "strict": true, 13 | "noImplicitReturns": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true 8 | }, 9 | "search.exclude": { 10 | "**/lib": true, 11 | "**/bin": true 12 | }, 13 | "editor.formatOnSave": true, 14 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 15 | "eslint.format.enable": true 16 | } 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /.github/workflows/validatePR.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | pr-validation: 9 | uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main 10 | format-check: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install modules 15 | run: yarn 16 | - name: Check Format 17 | run: yarn format 18 | -------------------------------------------------------------------------------- /test/soql/highlight_partial_from.soql: -------------------------------------------------------------------------------- 1 | -- SYNTAX TEST "source.soql" "highlight partial FROM testcase" 2 | 3 | SELECT Id, Name FROM 4 | -- ^^^^^^ source.soql keyword.operator.query.select.apex 5 | -- ^^ source.soql keyword.query.field.apex 6 | -- ^ source.soql punctuation.separator.comma.apex 7 | -- ^^^^ source.soql keyword.query.field.apex 8 | -- ^^^^ source.soql keyword.operator.query.from.apex 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # TypeScript generated files 2 | dist/ 3 | out/ 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Dependency directories 14 | node_modules/ 15 | jspm_packages/ 16 | 17 | # Optional npm cache directory 18 | .npm 19 | 20 | # Output of 'npm pack' 21 | *.tgz 22 | 23 | # Eclipse 24 | .project 25 | 26 | # MacOS folder atttribute tracking 27 | **/.DS_Store 28 | 29 | **/.sfdx 30 | **.sf -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": ["dbaeumer.vscode-eslint"], 7 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 8 | "unwantedRecommendations": [] 9 | } 10 | -------------------------------------------------------------------------------- /test/soql/simple_account.soql: -------------------------------------------------------------------------------- 1 | -- SYNTAX TEST "source.soql" "simple testcase" 2 | 3 | SELECT Id, Name FROM Account 4 | -- ^^^^^^ source.soql keyword.operator.query.select.apex 5 | -- ^^ source.soql keyword.query.field.apex 6 | -- ^ source.soql punctuation.separator.comma.apex 7 | -- ^^^^ source.soql keyword.query.field.apex 8 | -- ^^^^ source.soql keyword.operator.query.from.apex 9 | -- ^ source.soql 10 | -- ^^^^^^^ source.soql storage.type.apex 11 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-COUNT.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT COUNT() FROM Contact 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^^ source.soql support.function.query.apex 5 | # ^ source.soql punctuation.parenthesis.open.apex 6 | # ^ source.soql punctuation.parenthesis.close.apex 7 | # ^ source.soql 8 | # ^^^^ source.soql keyword.operator.query.from.apex 9 | # ^ source.soql 10 | # ^^^^^^^ source.soql storage.type.apex 11 | > -------------------------------------------------------------------------------- /test/soql/snapshots/simple_account.soql.snap: -------------------------------------------------------------------------------- 1 | > 2 | >SELECT Id, Name FROM Account 3 | #^^^^^^ source.soql keyword.operator.query.select.apex 4 | # ^ source.soql 5 | # ^^ source.soql keyword.query.field.apex 6 | # ^ source.soql punctuation.separator.comma.apex 7 | # ^ source.soql 8 | # ^^^^ source.soql keyword.query.field.apex 9 | # ^ source.soql 10 | # ^^^^ source.soql keyword.operator.query.from.apex 11 | # ^ source.soql 12 | # ^^^^^^^ source.soql storage.type.apex 13 | > 14 | > -------------------------------------------------------------------------------- /.github/workflows/onRelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | # when a github release happens, publish an npm package, 4 | on: 5 | release: 6 | types: [released] 7 | # support manual release 8 | workflow_dispatch: 9 | inputs: 10 | tag: 11 | description: tag that needs to publish 12 | type: string 13 | required: true 14 | jobs: 15 | npm: 16 | uses: salesforcecli/github-workflows/.github/workflows/npmPublish.yml@main 17 | with: 18 | ctc: false 19 | githubTag: ${{ github.event.release.tag_name || inputs.tag }} 20 | secrets: inherit 21 | -------------------------------------------------------------------------------- /test/soql/header-comments.soql: -------------------------------------------------------------------------------- 1 | -- SYNTAX TEST "source.soql" "Line comments allowed at top of file only" 2 | 3 | // Header comments 4 | -- ^^^^^^^^^^^^^^^^^^ source.soql comment.line 5 | // may have leading spaces 6 | -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ source.soql comment.line 7 | 8 | // and empty lines 9 | -- ^^^^^^^^^^^^^^^^^^ source.soql comment.line 10 | 11 | SELECT Id, Name 12 | -- ^^^^^^^^^^^^^^^ source.soql 13 | FROM Account 14 | -- ^^^^^^^^^^^^ source.soql 15 | // comment not allowed within query 16 | -- ^^ source.soql keyword.operator.arithmetic.apex 17 | -- ^ source.soql 18 | WHERE Name != 'Booh' 19 | -- ^^^^^ source.soql keyword.operator.query.where.apex 20 | -------------------------------------------------------------------------------- /src/soql.tmLanguage.template.yml: -------------------------------------------------------------------------------- 1 | # This is an incomplete grammar used as a template at build time. 2 | # The build script uses this grammar and the full Apex language grammar as input to generate 3 | # a grammar to be used for standalone SOQL editors 4 | # (basically using only SOQL query expression at the top-level pattern) 5 | --- 6 | name: SOQL 7 | scopeName: source.soql 8 | fileTypes: [soql] 9 | uuid: 1CDD8B23-7C84-46AA-9302-476968A79A83 10 | 11 | patterns: 12 | - include: '#soqlHeaderComment' 13 | - include: '#soql-query-expression' 14 | 15 | repository: # All the Apex-language grammar repository rules merged here 16 | soqlHeaderComment: 17 | name: comment.line 18 | begin: ^\s*//.*$ 19 | while: ^\s*//.*$ 20 | 21 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-simple-query.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Id, Name, BillingCity FROM Account 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^ source.soql keyword.query.field.apex 8 | # ^ source.soql punctuation.separator.comma.apex 9 | # ^ source.soql 10 | # ^^^^^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^ source.soql storage.type.apex 15 | > -------------------------------------------------------------------------------- /.github/workflows/slackNotify.yml: -------------------------------------------------------------------------------- 1 | name: Slack Notification for Github Actions 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Push to main 7 | - release 8 | types: 9 | - completed 10 | 11 | jobs: 12 | notify: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Announce result 16 | uses: slackapi/slack-github-action@v1.22.0 17 | with: 18 | payload: | 19 | { 20 | "text": "GitHub Actions Notification", 21 | "event": "${{ github.event.workflow_run.name }}, run: ${{ github.event.workflow_run.html_url }}", 22 | "repo": "${{ github.event.workflow_run.repository.name }}", 23 | "result": "${{ github.event.workflow_run.conclusion }}" 24 | } 25 | env: 26 | SLACK_WEBHOOK_URL: ${{ secrets.IDEE_RELEASE_ALERT_SLACK_WEBHOOK }} 27 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/AnnotationOnSameLine_PR68_PR69.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #68/#69: Annotation on same line as method declaration 3 | * 4 | * This class demonstrates annotations on the same line as method declarations. 5 | * The syntax distinguishes annotations from method modifiers. 6 | */ 7 | public class AnnotationOnSameLine_PR68_PR69 { 8 | 9 | @Future(callout=true) public static void futureMethod() { 10 | System.debug('Future method'); 11 | } 12 | 13 | @InvocableMethod(label='Test') public static void invocableMethod() { 14 | System.debug('Invocable method'); 15 | } 16 | 17 | @TestVisible private void testVisibleMethod() { 18 | System.debug('Test visible method'); 19 | } 20 | 21 | @deprecated public void deprecatedMethod() { 22 | System.debug('Deprecated method'); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/syntax.md: -------------------------------------------------------------------------------- 1 | ## Important regular expressions: 2 | 3 | #### Identifier 4 | 5 | - Expression: `[_[:alpha:]][_[:alnum:]]*` 6 | - Matches: `_`, `Ident42` 7 | 8 | #### Type name 9 | 10 | ``` 11 | (? 12 | (?: 13 | (?:ref\s+)? # only in certain place with ref local/return 14 | (?: 15 | (?:(?[_[:alpha:]][_[:alnum:]]*)\s*\:\:\s*)? # alias-qualification 16 | (? # identifier + type arguments (if any) 17 | \g\s* 18 | (?\s*<(?:[^<>]|\g)+>\s*)? 19 | ) 20 | (?:\s*\.\s*\g)* | # Are there any more names being dotted into? 21 | (?\s*\((?:[^\(\)]|\g)+\)) 22 | ) 23 | (?:\s*\*\s*)* # pointer suffix? 24 | (?:\s*\?\s*)? # nullable suffix? 25 | (?:\s*\[(?:\s*,\s*)*\]\s*)* # array suffix? 26 | ) 27 | ) 28 | ``` 29 | -------------------------------------------------------------------------------- /scripts/build-grammars.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const js_yaml = require('js-yaml'); 4 | const plist = require('plist'); 5 | 6 | const inputGrammar = 'src/apex.tmLanguage.yml'; 7 | const grammarsDirectory = 'grammars/'; 8 | 9 | function handleError(err) { 10 | console.error(err.toString()); 11 | process.exit(-1); 12 | } 13 | 14 | try { 15 | // Create grammars directory if it doesn't exist 16 | if (!fs.existsSync(grammarsDirectory)) { 17 | fs.mkdirSync(grammarsDirectory); 18 | } 19 | 20 | // Read and parse YAML 21 | const text = fs.readFileSync(inputGrammar); 22 | const jsonData = js_yaml.load(text); 23 | 24 | // Convert to plist 25 | const plistData = plist.build(jsonData); 26 | 27 | // Write the output file 28 | fs.writeFileSync(path.join(grammarsDirectory, 'apex.tmLanguage'), plistData); 29 | console.log('Successfully built TextMate grammar'); 30 | } catch (err) { 31 | handleError(err); 32 | } 33 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-ORDERY_BY.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name FROM Account ORDER BY Name DESC NULLS LAST 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql 6 | # ^^^^ source.soql keyword.operator.query.from.apex 7 | # ^ source.soql 8 | # ^^^^^^^ source.soql storage.type.apex 9 | # ^ source.soql 10 | # ^^^^^^^^ source.soql keyword.operator.query.orderby.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.query.field.apex 13 | # ^ source.soql 14 | # ^^^^ source.soql keyword.operator.query.descending.apex 15 | # ^ source.soql 16 | # ^^^^^^^^^^ source.soql keyword.operator.query.nullslast.apex 17 | 18 | > -------------------------------------------------------------------------------- /scripts/build-atom.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const js_yaml = require('js-yaml'); 4 | const cson = require('cson-parser'); 5 | 6 | const inputGrammar = 'src/apex.tmLanguage.yml'; 7 | const grammarsDirectory = 'grammars/'; 8 | 9 | function handleError(err) { 10 | console.error(err.toString()); 11 | process.exit(-1); 12 | } 13 | 14 | try { 15 | // Create grammars directory if it doesn't exist 16 | if (!fs.existsSync(grammarsDirectory)) { 17 | fs.mkdirSync(grammarsDirectory); 18 | } 19 | 20 | // Read and parse YAML 21 | const text = fs.readFileSync(inputGrammar); 22 | const jsonData = js_yaml.load(text); 23 | 24 | // Convert to CSON with pretty formatting - using default output 25 | const csonData = cson.stringify(jsonData, null, 2); 26 | 27 | // Write the output file 28 | fs.writeFileSync( 29 | path.join(grammarsDirectory, 'apex.tmLanguage.cson'), 30 | csonData 31 | ); 32 | console.log('Successfully built Atom grammar'); 33 | } catch (err) { 34 | handleError(err); 35 | } 36 | -------------------------------------------------------------------------------- /docs/coding-guidelines.md: -------------------------------------------------------------------------------- 1 | # Coding Guidelines 2 | 3 | When possible, the following are enforced through the code formatter 4 | (Prettier.js) and tslint rules. 5 | 6 | --- 7 | 8 | ## Indentation 9 | 10 | We use spaces, not tabs. 11 | 12 | ## Names 13 | 14 | - Use PascalCase for `type` names 15 | - Use UPPERCASE_WITH_SPACES for `enum` values and constants 16 | - Use camelCase for `function` and `method` names 17 | - Use camelCase for `property` names and `local variables` 18 | - Use whole words in names when possible 19 | - Use camelCase for file names (name files after the main Type it exports) 20 | 21 | ## Conventions 22 | 23 | - Create a folder for each major subarea 24 | - In the folder, create an index.ts which exports the public facing API for that 25 | subarea. 26 | - Tests can refer directly to the .ts files; other consumers should refer to the 27 | index.ts file. 28 | 29 | ## Comments 30 | 31 | - Use sparingly since comments always become outdated quickly. 32 | - If you must, use JSDoc style comments. 33 | 34 | ## Strings 35 | 36 | - Use 'single quotes' 37 | - All strings visible to the user need to be externalized in a `messages.ts` file. 38 | 39 | ## null and undefined 40 | 41 | Use `undefined`, do not use `null`. 42 | -------------------------------------------------------------------------------- /test/soql/snapshots/example_GROUP_BY.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT LeadSource, COUNT(Name) FROM Lead GROUP BY LeadSource 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^^^^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^ source.soql support.function.query.apex 8 | # ^ source.soql punctuation.parenthesis.open.apex 9 | # ^^^^ source.soql keyword.query.field.apex 10 | # ^ source.soql punctuation.parenthesis.close.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^ source.soql storage.type.apex 15 | # ^ source.soql 16 | # ^^^^^^^^ source.soql keyword.operator.query.apex 17 | # ^ source.soql 18 | # ^^^^^^^^^^ source.soql keyword.query.field.apex 19 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-OFFSET_ORDER_BY.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, Id FROM Merchandise__c ORDER BY Name OFFSET 100 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^ source.soql keyword.query.field.apex 8 | # ^ source.soql 9 | # ^^^^ source.soql keyword.operator.query.from.apex 10 | # ^ source.soql 11 | # ^^^^^^^^^^^^^^ source.soql storage.type.apex 12 | # ^ source.soql 13 | # ^^^^^^^^ source.soql keyword.operator.query.orderby.apex 14 | # ^ source.soql 15 | # ^^^^ source.soql keyword.query.field.apex 16 | # ^ source.soql 17 | # ^^^^^^ source.soql keyword.operator.query.apex 18 | # ^ source.soql 19 | # ^^^ source.soql constant.numeric.decimal.apex 20 | > -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Auto-Merge 2 | 3 | on: 4 | # Manual trigger for testing/debugging 5 | workflow_dispatch: 6 | inputs: 7 | skipCI: 8 | description: 'Skip CI checks and merge immediately' 9 | required: false 10 | default: false 11 | type: boolean 12 | 13 | # Schedule trigger - runs every hour during off-hours 14 | schedule: 15 | - cron: '45 2,5,8,11 * * *' 16 | 17 | permissions: 18 | contents: write # Required to merge PRs 19 | pull-requests: write # Required to manage PRs 20 | checks: read # Required to read check statuses 21 | actions: read # Required to read workflow run information 22 | 23 | jobs: 24 | automerge: 25 | # Only run for dependabot PRs (when checks complete), manual triggers, or scheduled runs 26 | if: github.actor == 'dependabot[bot]' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' 27 | uses: salesforcecli/github-workflows/.github/workflows/automerge.yml@main 28 | secrets: 29 | SVC_CLI_BOT_GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} 30 | with: 31 | mergeMethod: squash 32 | skipCI: ${{ github.event.inputs.skipCI == 'true' }} 33 | -------------------------------------------------------------------------------- /test/soql/timestamp_literals.soql: -------------------------------------------------------------------------------- 1 | -- SYNTAX TEST "source.soql" "timestamp literals" 2 | 3 | SELECT UserId FROM LoginHistory WHERE LoginTime > 2010-09-20T22:16:30-03:00 AND LoginTime < 2021-09-21T22:16:30Z; 4 | -- ^^^^^^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 5 | -- ^ source.soql 6 | -- ^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 7 | -- ^ source.soql 8 | 9 | 10 | SELECT UserId FROM LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000-03:00 AND LoginTime < 2021-09-21T22:16:30.000Z; 11 | -- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 12 | -- ^ source.soql 13 | -- ^^^^^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 14 | -- ^ source.soql 15 | 16 | -------------------------------------------------------------------------------- /.github/workflows/manualRelease.yml: -------------------------------------------------------------------------------- 1 | name: Manual Release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | release: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | with: 12 | token: ${{ secrets.IDEE_GH_TOKEN }} 13 | - name: Conventional Changelog Action 14 | id: changelog 15 | uses: TriPSs/conventional-changelog-action@d360fad3a42feca6462f72c97c165d60a02d4bf2 16 | # overriding some of the basic behaviors to just get the changelog 17 | with: 18 | git-user-name: Release Bot 19 | git-user-email: ${{ secrets.IDEE_GH_EMAIL }} 20 | github-token: ${{ secrets.IDEE_GH_TOKEN }} 21 | output-file: false 22 | # always do the release, even if there are no semantic commits 23 | skip-on-empty: false 24 | tag-prefix: '' 25 | - uses: notiz-dev/github-action-json-property@2192e246737701f108a4571462b76c75e7376216 26 | id: packageVersion 27 | with: 28 | path: 'package.json' 29 | prop_path: 'version' 30 | - name: Create Github Release 31 | uses: actions/create-release@v1 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} 34 | with: 35 | tag_name: ${{ steps.packageVersion.outputs.prop }} 36 | release_name: ${{ steps.packageVersion.outputs.prop }} 37 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/TernaryExpressions_PR70.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #70: Ternary expression syntax highlighting 3 | * 4 | * This class demonstrates nested ternary expressions and ternary with method calls. 5 | * The syntax highlighting should properly distinguish nested conditionals. 6 | */ 7 | public class TernaryExpressions_PR70 { 8 | 9 | public void simpleTernary() { 10 | Integer result = x ? 19 : 23; 11 | } 12 | 13 | public void nestedTernary() { 14 | // Nested ternary expression - issue #43 15 | Integer result = x ? y ? 1 : 2 : 3; 16 | } 17 | 18 | public void ternaryWithMethodCalls() { 19 | // Ternary with method call - issue #43 20 | String s = condition ? getValue() : getDefault(); 21 | } 22 | 23 | public void ternaryAsArgument() { 24 | processValue(x ? 19 : 23); 25 | } 26 | 27 | public void complexNestedTernary() { 28 | String output = a ? (b ? 'both' : 'a only') : (c ? 'c only' : 'neither'); 29 | } 30 | 31 | private String getValue() { return 'value'; } 32 | private String getDefault() { return 'default'; } 33 | private void processValue(Integer val) { } 34 | 35 | private Boolean x; 36 | private Boolean y; 37 | private Boolean a; 38 | private Boolean b; 39 | private Boolean c; 40 | private Boolean condition; 41 | } 42 | 43 | -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/DMLOnMethodCallResults_PR71.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #71: DML operations on method call results 3 | * 4 | * This class demonstrates DML operations performed on method call results. 5 | * The syntax highlighting should recognize DML keywords when followed by method calls. 6 | */ 7 | public class DMLOnMethodCallResults_PR71 { 8 | 9 | public void dmlOnMapValues() { 10 | // Issue #26: DML on Map.values() receives same scope as direct insert 11 | Map accounts = new Map(); 12 | insert accounts.values(); 13 | insert new List(); 14 | } 15 | 16 | public void dmlOnQueryResults() { 17 | List accounts = [SELECT Id FROM Account]; 18 | update accounts; 19 | delete [SELECT Id FROM Contact]; 20 | } 21 | 22 | public void dmlOnMethodReturn() { 23 | List accounts = getAccounts(); 24 | upsert accounts; 25 | undelete getDeletedAccounts(); 26 | } 27 | 28 | public void dmlOnChainedMethods() { 29 | insert getAccounts().clone(); 30 | update new Map(getAccounts()); 31 | } 32 | 33 | private List getAccounts() { 34 | return new List(); 35 | } 36 | 37 | private List getDeletedAccounts() { 38 | return new List(); 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-LIMIT.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name FROM Account WHERE Industry = 'media' LIMIT 125 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql 6 | # ^^^^ source.soql keyword.operator.query.from.apex 7 | # ^ source.soql 8 | # ^^^^^^^ source.soql storage.type.apex 9 | # ^ source.soql 10 | # ^^^^^ source.soql keyword.operator.query.where.apex 11 | # ^ source.soql 12 | # ^^^^^^^^ source.soql keyword.query.field.apex 13 | # ^ source.soql 14 | # ^ source.soql keyword.operator.assignment.apex 15 | # ^ source.soql 16 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 17 | # ^^^^^ source.soql string.quoted.single.apex 18 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 19 | # ^ source.soql 20 | # ^^^^^ source.soql keyword.operator.query.apex 21 | # ^ source.soql 22 | # ^^^ source.soql constant.numeric.decimal.apex 23 | > -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/NamespaceQualifiedTypes_PR72.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #72: Support namespace-qualified types in extends/implements 3 | * 4 | * This class demonstrates classes extending and implementing namespace-qualified types. 5 | * The syntax highlighting should properly parse System.Exception and Database.Batchable. 6 | */ 7 | public class NamespaceQualifiedTypes_PR72 extends System.Exception { 8 | // Class extends namespace-qualified type - issue #50 9 | } 10 | 11 | // Another example extending namespace-qualified type 12 | public class MyCustomException_PR72 extends System.Exception { 13 | public MyCustomException_PR72(String message) { 14 | super(message); 15 | } 16 | } 17 | 18 | // Class implementing namespace-qualified type - issue #50 19 | public class BatchableProcessor_PR72 implements Database.Batchable { 20 | public Database.QueryLocator start(Database.BatchableContext bc) { 21 | return Database.getQueryLocator('SELECT Id FROM Account'); 22 | } 23 | 24 | public void execute(Database.BatchableContext bc, List scope) { 25 | // Process accounts 26 | Integer foo = 0; 27 | } 28 | 29 | public void finish(Database.BatchableContext bc) { 30 | // Finish processing 31 | } 32 | } 33 | 34 | // Class extending and implementing namespace-qualified types 35 | public class ComplexExample_PR72 extends System.OctopusException implements Database.Stateful { 36 | private Integer state; 37 | 38 | public ComplexExample_PR72(String message) { 39 | super(message); 40 | } 41 | } 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | License for modifications and not otherwise marked: 2 | 3 | Copyright (c) 2018, Salesforce.com, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, this 13 | list of conditions and the following disclaimer in the documentation and/or 14 | other materials provided with the distribution. 15 | 16 | * Neither the name of Salesforce.com nor the names of its contributors may be 17 | used to endorse or promote products derived from this software without specific 18 | prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /scripts/build-soql.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const js_yaml = require('js-yaml'); 4 | const plist = require('plist'); 5 | 6 | const inputGrammar = 'src/apex.tmLanguage.yml'; 7 | const inputSoqlGrammarTemplate = 'src/soql.tmLanguage.template.yml'; 8 | const grammarsDirectory = 'grammars/'; 9 | 10 | function handleError(err) { 11 | console.error(err.toString()); 12 | process.exit(-1); 13 | } 14 | 15 | try { 16 | // Create grammars directory if it doesn't exist 17 | if (!fs.existsSync(grammarsDirectory)) { 18 | fs.mkdirSync(grammarsDirectory); 19 | } 20 | 21 | // Read and parse both grammars 22 | const soqlGrammar = js_yaml.load(fs.readFileSync(inputSoqlGrammarTemplate)); 23 | const apexGrammar = js_yaml.load(fs.readFileSync(inputGrammar)); 24 | 25 | // Merge the repository of rules from Apex grammar 26 | soqlGrammar['repository'] = Object.assign( 27 | {}, 28 | apexGrammar.repository, 29 | soqlGrammar['repository'] 30 | ); 31 | 32 | // Remove the comments rule SOQL query expression 33 | const apexGrammarSoqlExpressionPatterns = 34 | apexGrammar['repository']['soql-query-expression']['patterns']; 35 | soqlGrammar['repository']['soql-query-expression']['patterns'] = 36 | apexGrammarSoqlExpressionPatterns.filter( 37 | (pattern) => pattern.include !== '#comment' 38 | ); 39 | 40 | // Convert to plist and write 41 | const plistData = plist.build(soqlGrammar); 42 | fs.writeFileSync(path.join(grammarsDirectory, 'soql.tmLanguage'), plistData); 43 | console.log('Successfully built SOQL grammar'); 44 | } catch (err) { 45 | handleError(err); 46 | } 47 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-OFFSET_ORDER_BY_LIMIT.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, Id FROM Merchandise__c ORDER BY Name LIMIT 20 OFFSET 100 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^ source.soql keyword.query.field.apex 8 | # ^ source.soql 9 | # ^^^^ source.soql keyword.operator.query.from.apex 10 | # ^ source.soql 11 | # ^^^^^^^^^^^^^^ source.soql storage.type.apex 12 | # ^ source.soql 13 | # ^^^^^^^^ source.soql keyword.operator.query.orderby.apex 14 | # ^ source.soql 15 | # ^^^^ source.soql keyword.query.field.apex 16 | # ^ source.soql 17 | # ^^^^^ source.soql keyword.operator.query.apex 18 | # ^ source.soql 19 | # ^^ source.soql constant.numeric.decimal.apex 20 | # ^ source.soql 21 | # ^^^^^^ source.soql keyword.operator.query.apex 22 | # ^ source.soql 23 | # ^^^ source.soql constant.numeric.decimal.apex 24 | > -------------------------------------------------------------------------------- /test/incomplete-code.test.ts: -------------------------------------------------------------------------------- 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('Incomplete code', () => { 16 | it("Don't eat the next lines if there isn't a semicolon (issue #15)", async () => { 17 | const input = Input.InClass(` 18 | private String _color 19 | public ColorTest(String white) 20 | { 21 | _color = white; 22 | } 23 | `); 24 | 25 | let tokens = await tokenize(input); 26 | 27 | tokens.should.deep.equal([ 28 | Token.Keywords.Modifiers.Private, 29 | Token.PrimitiveType.String, 30 | Token.Identifiers.PropertyName('_color'), 31 | Token.Keywords.Modifiers.Public, 32 | Token.Identifiers.MethodName('ColorTest'), 33 | Token.Punctuation.OpenParen, 34 | Token.PrimitiveType.String, 35 | Token.Identifiers.ParameterName('white'), 36 | Token.Punctuation.CloseParen, 37 | Token.Punctuation.OpenBrace, 38 | Token.Variables.ReadWrite('_color'), 39 | Token.Operators.Assignment, 40 | Token.Variables.ReadWrite('white'), 41 | Token.Punctuation.Semicolon, 42 | Token.Punctuation.CloseBrace, 43 | ]); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/utils/registry.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as vscodeTM from 'vscode-textmate'; 4 | import * as oniguruma from 'vscode-oniguruma'; 5 | 6 | const grammarPaths = { 7 | apex: path.resolve(__dirname, '../../../grammars/apex.tmLanguage'), 8 | soql: path.resolve(__dirname, '../../../grammars/soql.tmLanguage'), 9 | }; 10 | 11 | const wasmBin = fs.readFileSync( 12 | path.resolve( 13 | process.cwd(), 14 | './node_modules/vscode-oniguruma/release/onig.wasm' 15 | ) 16 | ).buffer; 17 | 18 | const onigLibPromise = oniguruma.loadWASM(wasmBin).then(() => { 19 | return { 20 | createOnigScanner(patterns) { 21 | return new oniguruma.OnigScanner(patterns); 22 | }, 23 | createOnigString(s) { 24 | return new oniguruma.OnigString(s); 25 | }, 26 | }; 27 | }); 28 | 29 | export class TMRegistry { 30 | public grammars: { [key: string]: vscodeTM.IGrammar }; 31 | public registry: vscodeTM.Registry; 32 | 33 | constructor() { 34 | this.grammars = {}; 35 | this.registry = new vscodeTM.Registry({ 36 | onigLib: onigLibPromise, 37 | loadGrammar: (scopeName) => this.loadGrammar(scopeName), 38 | }); 39 | } 40 | 41 | loadGrammar(scopeName): Promise { 42 | const grammarPath = grammarPaths[scopeName]; 43 | if (this.grammars[scopeName]) { 44 | return new Promise((resolve, reject) => { 45 | resolve(this.grammars[scopeName]); 46 | }); 47 | } 48 | return new Promise((resolve, reject) => { 49 | const content = fs.readFileSync(grammarPath); 50 | const rawGrammar = vscodeTM.parseRawGrammar(content.toString()); 51 | this.registry.addGrammar(rawGrammar).then((ig) => { 52 | resolve((this.grammars[scopeName] = ig)); 53 | }); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-WHERE.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Id FROM Contact WHERE Name LIKE 'A%' AND MailingCity = 'California' 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^ source.soql keyword.query.field.apex 5 | # ^ source.soql 6 | # ^^^^ source.soql keyword.operator.query.from.apex 7 | # ^ source.soql 8 | # ^^^^^^^ source.soql storage.type.apex 9 | # ^ source.soql 10 | # ^^^^^ source.soql keyword.operator.query.where.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.query.field.apex 13 | # ^ source.soql 14 | # ^^^^ source.soql keyword.operator.query.apex 15 | # ^ source.soql 16 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 17 | # ^^ source.soql string.quoted.single.apex 18 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 19 | # ^ source.soql 20 | # ^^^ source.soql keyword.operator.query.apex 21 | # ^ source.soql 22 | # ^^^^^^^^^^^ source.soql keyword.query.field.apex 23 | # ^ source.soql 24 | # ^ source.soql keyword.operator.assignment.apex 25 | # ^ source.soql 26 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 27 | # ^^^^^^^^^^ source.soql string.quoted.single.apex 28 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 29 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-HAVING.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^ source.soql support.function.query.apex 8 | # ^ source.soql punctuation.parenthesis.open.apex 9 | # ^^ source.soql keyword.query.field.apex 10 | # ^ source.soql punctuation.parenthesis.close.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql 16 | # ^^^^^^^^ source.soql keyword.operator.query.apex 17 | # ^ source.soql 18 | # ^^^^ source.soql keyword.query.field.apex 19 | # ^ source.soql 20 | # ^^^^^^ source.soql keyword.operator.query.apex 21 | # ^ source.soql 22 | # ^^^^^ source.soql support.function.query.apex 23 | # ^ source.soql punctuation.parenthesis.open.apex 24 | # ^^ source.soql keyword.query.field.apex 25 | # ^ source.soql punctuation.parenthesis.close.apex 26 | # ^ source.soql 27 | # ^ source.soql keyword.operator.relational.apex 28 | # ^ source.soql 29 | # ^ source.soql constant.numeric.decimal.apex 30 | > -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/FinalKeywordInMethodParams_PR67.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #67: Support final keyword in method parameters 3 | * 4 | * This class demonstrates methods with final keyword in parameters. 5 | * The syntax highlighting should properly recognize the final modifier. 6 | */ 7 | public class FinalKeywordInMethodParams_PR67 { 8 | 9 | public void methodWithFinalParameter(final String param) { 10 | System.debug(param); 11 | } 12 | 13 | public void methodWithMultipleFinalParams(final String str, final Integer num) { 14 | System.debug(str + num); 15 | } 16 | 17 | public void methodWithFinalAndRegular(final String finalParam, String regularParam) { 18 | System.debug(finalParam + regularParam); 19 | } 20 | 21 | public static void staticMethodWithFinal(final Boolean flag) { 22 | System.debug(flag); 23 | } 24 | 25 | /** 26 | * Test case: final keyword should not unhighlight the type that follows it. 27 | * Both types (with and without final) should have consistent syntax highlighting. 28 | * Issue: When final precedes a type, the type loses its keyword highlighting. 29 | */ 30 | public void testFinalKeywordDoesNotUnhighlightType(final String str1, String str2) { 31 | // str1 parameter: final String - String should be highlighted as keyword 32 | // str2 parameter: String - String IS highlighted as keyword (correct) 33 | // Both String types should have identical highlighting 34 | System.debug(str1 + str2); 35 | } 36 | 37 | public void testMultipleTypesWithFinal(final Integer num, final Boolean flag, final Decimal dec) { 38 | // All three types (Integer, Boolean, Decimal) should be highlighted as keywords 39 | // even though they follow the final keyword 40 | System.debug(num + ' ' + flag + ' ' + dec); 41 | } 42 | 43 | public void contrastTest(String regularType, final String finalType) { 44 | // regularType: String is highlighted (correct) 45 | // finalType: String should also be highlighted (but currently isn't) 46 | // This demonstrates the highlighting inconsistency 47 | System.debug(regularType.equals(finalType)); 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-child-to-parent-CUSTOM_OBJECTS.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Id, FirstName__c, Mother_of_Child__r.FirstName__c FROM Daughter__c WHERE Mother_of_Child__r.LastName__c LIKE 'C%' 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^^^^^^^^ source.soql keyword.query.field.apex 8 | # ^ source.soql punctuation.separator.comma.apex 9 | # ^ source.soql 10 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql 16 | # ^^^^^ source.soql keyword.operator.query.where.apex 17 | # ^ source.soql 18 | # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 19 | # ^ source.soql 20 | # ^^^^ source.soql keyword.operator.query.apex 21 | # ^ source.soql 22 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 23 | # ^^ source.soql string.quoted.single.apex 24 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 25 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-parent-to-child_CUSTOM_OBJECTS.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, (SELECT Name FROM Line_Items__r) FROM Merchandise__c WHERE Name LIKE ‘Acme%’ 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^ source.soql punctuation.parenthesis.open.apex 8 | # ^^^^^^ source.soql keyword.operator.query.select.apex 9 | # ^ source.soql 10 | # ^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql punctuation.parenthesis.close.apex 16 | # ^ source.soql 17 | # ^^^^ source.soql keyword.operator.query.from.apex 18 | # ^ source.soql 19 | # ^^^^^^^^^^^^^^ source.soql storage.type.apex 20 | # ^ source.soql 21 | # ^^^^^ source.soql keyword.operator.query.where.apex 22 | # ^ source.soql 23 | # ^^^^ source.soql keyword.query.field.apex 24 | # ^ source.soql 25 | # ^^^^ source.soql keyword.operator.query.apex 26 | # ^ source.soql 27 | # ^ source.soql 28 | # ^^^^ source.soql keyword.query.field.apex 29 | # ^ source.soql keyword.operator.arithmetic.apex 30 | # ^^ source.soql 31 | > -------------------------------------------------------------------------------- /test/local.test.ts: -------------------------------------------------------------------------------- 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('Locals', () => { 16 | it('declaration', async () => { 17 | const input = Input.InMethod(`Integer x;`); 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.PrimitiveType.Integer, 22 | Token.Identifiers.LocalName('x'), 23 | Token.Punctuation.Semicolon, 24 | ]); 25 | }); 26 | 27 | it('declaration with initializer', async () => { 28 | const input = Input.InMethod(`Integer x = 42;`); 29 | const tokens = await tokenize(input); 30 | 31 | tokens.should.deep.equal([ 32 | Token.PrimitiveType.Integer, 33 | Token.Identifiers.LocalName('x'), 34 | Token.Operators.Assignment, 35 | Token.Literals.Numeric.Decimal('42'), 36 | Token.Punctuation.Semicolon, 37 | ]); 38 | }); 39 | 40 | it('multiple declarators', async () => { 41 | const input = Input.InMethod(`Integer x, y;`); 42 | const tokens = await tokenize(input); 43 | 44 | tokens.should.deep.equal([ 45 | Token.PrimitiveType.Integer, 46 | Token.Identifiers.LocalName('x'), 47 | Token.Punctuation.Comma, 48 | Token.Identifiers.LocalName('y'), 49 | Token.Punctuation.Semicolon, 50 | ]); 51 | }); 52 | 53 | it('multiple declarators with initializers', async () => { 54 | const input = Input.InMethod(`Integer x = 19, y = 23;`); 55 | const tokens = await tokenize(input); 56 | 57 | tokens.should.deep.equal([ 58 | Token.PrimitiveType.Integer, 59 | Token.Identifiers.LocalName('x'), 60 | Token.Operators.Assignment, 61 | Token.Literals.Numeric.Decimal('19'), 62 | Token.Punctuation.Comma, 63 | Token.Identifiers.LocalName('y'), 64 | Token.Operators.Assignment, 65 | Token.Literals.Numeric.Decimal('23'), 66 | Token.Punctuation.Semicolon, 67 | ]); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@salesforce/apex-tmlanguage", 3 | "version": "2.0.1", 4 | "description": "Textmate grammar for Apex with outputs for VSCode, Atom and TextMate.", 5 | "displayName": "apex-tmLanguage", 6 | "keywords": [ 7 | "salesforce-dx", 8 | "salesforce", 9 | "apex" 10 | ], 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/forcedotcom/apex-tmLanguage/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/forcedotcom/apex-tmLanguage.git" 18 | }, 19 | "contributors": [ 20 | { 21 | "name": "Nathan Totten", 22 | "url": "https://github.com/ntotten" 23 | }, 24 | { 25 | "name": "Luis Campos-Guajardo", 26 | "url": "https://github.com/lcampos" 27 | }, 28 | { 29 | "name": "Nick Chen", 30 | "url": "https://github.com/vazexqi" 31 | } 32 | ], 33 | "scripts": { 34 | "build:grammars": "node scripts/build-grammars.js", 35 | "build:atom": "node scripts/build-atom.js", 36 | "build:soql": "node scripts/build-soql.js", 37 | "build": "npm run build:grammars && npm run build:atom && npm run build:soql", 38 | "compile": "tsc -p .", 39 | "watch": "tsc -w -p .", 40 | "test:soql-tmgrammar": "vscode-tmgrammar-test -g \"./grammars/soql.tmLanguage\" \"./test/soql/*.soql\" ", 41 | "test:soql-tmgrammar-snapshots": "vscode-tmgrammar-snap -s source.soql -g \"./grammars/soql.tmLanguage\" \"./test/soql/snapshots/*.soql\" ", 42 | "test": "npm run compile && mocha out/test/**/*.test.js && npm run test:soql-tmgrammar && npm run test:soql-tmgrammar-snapshots", 43 | "prepare": "npm run build", 44 | "format": "prettier --config .prettierrc.json --write './**/*.{ts,js,json,md}'" 45 | }, 46 | "files": [ 47 | "grammars/**", 48 | "LICENSE.md", 49 | "README.md" 50 | ], 51 | "engines": { 52 | "node": ">=20" 53 | }, 54 | "devDependencies": { 55 | "@commitlint/cli": "^20", 56 | "@commitlint/config-conventional": "^20", 57 | "@types/chai": "4.3.4", 58 | "@types/mocha": "^10.0.10", 59 | "chai": "^4.3.7", 60 | "cson-parser": "^4.0.9", 61 | "cz-conventional-changelog": "^3.3.0", 62 | "js-yaml": "^4.1.1", 63 | "mocha": "^11.7.5", 64 | "plist": "^3.1.0", 65 | "prettier": "^3.6.2", 66 | "typescript": "4.9.3", 67 | "vscode-textmate": "^9.2.1", 68 | "vscode-tmgrammar-test": "^0.1.3" 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /test/soql/snapshots/example-ORDER_BY_LIMIT.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name FROM Account WHERE Industry = 'media' ORDER BY BillingPostalCode ASC NULLS LAST LIMIT 125 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql 6 | # ^^^^ source.soql keyword.operator.query.from.apex 7 | # ^ source.soql 8 | # ^^^^^^^ source.soql storage.type.apex 9 | # ^ source.soql 10 | # ^^^^^ source.soql keyword.operator.query.where.apex 11 | # ^ source.soql 12 | # ^^^^^^^^ source.soql keyword.query.field.apex 13 | # ^ source.soql 14 | # ^ source.soql keyword.operator.assignment.apex 15 | # ^ source.soql 16 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 17 | # ^^^^^ source.soql string.quoted.single.apex 18 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 19 | # ^ source.soql 20 | # ^^^^^^^^ source.soql keyword.operator.query.orderby.apex 21 | # ^ source.soql 22 | # ^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 23 | # ^ source.soql 24 | # ^^^ source.soql keyword.operator.query.ascending.apex 25 | # ^ source.soql 26 | # ^^^^^^^^^^ source.soql keyword.operator.query.nullslast.apex 27 | # ^ source.soql 28 | # ^^^^^ source.soql keyword.operator.query.apex 29 | # ^ source.soql 30 | # ^^^ source.soql constant.numeric.decimal.apex 31 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-child-to-parent.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Contact.FirstName, Contact.Account.Name FROM Contact; 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 8 | # ^ source.soql 9 | # ^^^^ source.soql keyword.operator.query.from.apex 10 | # ^ source.soql 11 | # ^^^^^^^ source.soql storage.type.apex 12 | # ^^ source.soql 13 | >SELECT Id, Name, Account.Name FROM Contact WHERE Account.Industry = 'media' 14 | #^^^^^^ source.soql keyword.operator.query.select.apex 15 | # ^ source.soql 16 | # ^^ source.soql keyword.query.field.apex 17 | # ^ source.soql punctuation.separator.comma.apex 18 | # ^ source.soql 19 | # ^^^^ source.soql keyword.query.field.apex 20 | # ^ source.soql punctuation.separator.comma.apex 21 | # ^ source.soql 22 | # ^^^^^^^^^^^^ source.soql keyword.query.field.apex 23 | # ^ source.soql 24 | # ^^^^ source.soql keyword.operator.query.from.apex 25 | # ^ source.soql 26 | # ^^^^^^^ source.soql storage.type.apex 27 | # ^ source.soql 28 | # ^^^^^ source.soql keyword.operator.query.where.apex 29 | # ^ source.soql 30 | # ^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 31 | # ^ source.soql 32 | # ^ source.soql keyword.operator.assignment.apex 33 | # ^ source.soql 34 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 35 | # ^^^^^ source.soql string.quoted.single.apex 36 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 37 | > -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/TernaryWithDecimals_W8095488.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * W-8095488: Ternary expression with decimals syntax highlighting 3 | * 4 | * When a ternary expression contains a decimal literal and spaces are excluded, 5 | * the question mark may be incorrectly tokenized as a safe navigation operator 6 | * instead of a ternary operator. 7 | */ 8 | public class TernaryWithDecimals_W8095488 { 9 | 10 | // With spaces - should work correctly 11 | public void ternaryWithSpaces() { 12 | Boolean b = true; 13 | Decimal d = b ? .34 : 0; 14 | Decimal i = b?.123 : 0; 15 | } 16 | 17 | // Without spaces - potential tokenization issue 18 | public void ternaryWithoutSpaces() { 19 | Boolean b = true; 20 | Decimal d = b?.34:0; 21 | Decimal i = b?.123:0; 22 | } 23 | 24 | // Mixed scenarios 25 | public void mixedScenarios() { 26 | Boolean b = true; 27 | 28 | // Space before question mark, no space after 29 | Decimal d1 = b ?.34:0; 30 | 31 | // No space before, space after 32 | Decimal d2 = b? .34:0; 33 | 34 | // Space after question mark, no space around colon 35 | Decimal d3 = b? .34:0; 36 | 37 | // All spaces 38 | Decimal d4 = b ? .34 : 0; 39 | 40 | // Integer values (for comparison) 41 | Integer i1 = b?1:0; 42 | Integer i2 = b ? 1 : 0; 43 | } 44 | 45 | // Safe navigation operator (for comparison) 46 | public void safeNavigationOperator() { 47 | String s = 'test'; 48 | Integer len = s?.length(); 49 | 50 | Account acc; 51 | String name = acc?.Name; 52 | } 53 | 54 | // Complex expressions 55 | public void complexExpressions() { 56 | Boolean b = true; 57 | 58 | // Ternary in switch 59 | switch on (b) { 60 | when true { 61 | Decimal d = b?.34:0; 62 | String s = 'SUCCESS'; 63 | } 64 | } 65 | 66 | // Nested ternary with decimals 67 | Decimal nested = b ? (b?.5:.25) : .0; 68 | 69 | // Ternary with decimal in variable declaration 70 | Decimal inline = b?.99:0; 71 | } 72 | 73 | // Property access vs ternary 74 | public void propertyVsTernary() { 75 | Boolean b = true; 76 | 77 | // These should be ternary operators, not safe navigation 78 | Decimal d = b?.34:0; 79 | Decimal i = b?.123:0; 80 | 81 | // This is safe navigation 82 | String type = this?.type; 83 | } 84 | 85 | String type = 'test'; 86 | } 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/soql/snapshots/some_functions.soql.snap: -------------------------------------------------------------------------------- 1 | > 2 | >SELECT city_c, State_c, COUNT(Employee_Name__C) Counts, COUNT_DISTINCT(Employee_Name__C) DistCounts 3 | #^^^^^^ source.soql keyword.operator.query.select.apex 4 | # ^ source.soql 5 | # ^^^^^^ source.soql keyword.query.field.apex 6 | # ^ source.soql punctuation.separator.comma.apex 7 | # ^ source.soql 8 | # ^^^^^^^ source.soql keyword.query.field.apex 9 | # ^ source.soql punctuation.separator.comma.apex 10 | # ^ source.soql 11 | # ^^^^^ source.soql support.function.query.apex 12 | # ^ source.soql punctuation.parenthesis.open.apex 13 | # ^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 14 | # ^ source.soql punctuation.parenthesis.close.apex 15 | # ^ source.soql 16 | # ^^^^^^ source.soql keyword.query.field.apex 17 | # ^ source.soql punctuation.separator.comma.apex 18 | # ^ source.soql 19 | # ^^^^^^^^^^^^^^ source.soql support.function.query.apex 20 | # ^ source.soql punctuation.parenthesis.open.apex 21 | # ^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 22 | # ^ source.soql punctuation.parenthesis.close.apex 23 | # ^ source.soql 24 | # ^^^^^^^^^^ source.soql keyword.query.field.apex 25 | > FROM Employee__C 26 | #^ source.soql 27 | # ^^^^ source.soql keyword.operator.query.from.apex 28 | # ^ source.soql 29 | # ^^^^^^^^^^^ source.soql storage.type.apex 30 | > GROUP BY ROLLUP (City__c, State__C) 31 | #^ source.soql 32 | # ^^^^^^^^^^^^^^^ source.soql support.function.query.apex 33 | # ^ source.soql 34 | # ^ source.soql punctuation.parenthesis.open.apex 35 | # ^^^^^^^ source.soql keyword.query.field.apex 36 | # ^ source.soql punctuation.separator.comma.apex 37 | # ^ source.soql 38 | # ^^^^^^^^ source.soql keyword.query.field.apex 39 | # ^ source.soql punctuation.parenthesis.close.apex 40 | > -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Apex Language Grammar 2 | 3 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 4 | 5 | ## Introduction 6 | 7 | This repository contains the source code for generating the language grammar files for Salesforce's Apex. 8 | 9 | ## Disclaimer 10 | 11 | Development and setup of this project has not been tested for Windows OS. You may see a node-gyp error - [follow the instrutions here to resolve it](https://github.com/nodejs/node-gyp/blob/master/README.md). 12 | 13 | ## Development 14 | 15 | To **build and test** install Node.js do the following: 16 | 17 | - Run `yarn install` to install any dependencies. 18 | - Run `yarn run build` to build using gulp. 19 | - Run `yarn run test` to run tests. 20 | 21 | Output grammars are output in the `grammars/` directory. 22 | 23 | To see the token changes from within the Salesforce VS Code Extensions: 24 | 25 | 1. Copy the `apex.tmLanguage` results into `../salesforcedx-vscode/packages/salesforcedx-vscode-apex/node_modules/@salesforce/apex-tmlanguage/grammars/apex.tmLanguage`. 26 | 2. From the `Command Palette` select `Developer: Inspect Editor Tokens and Scopes`. 27 | 28 | ### Adding grammar rules 29 | 30 | Token structure is based off of [Textmate's Language Grammar guidelines](https://manual.macromates.com/en/language_grammars) 31 | 32 | ### Tests for SOQL grammar 33 | 34 | For the standalone SOQL grammar, tests are executed with [vscode-tmgrammar-tests](https://github.com/PanAeon/vscode-tmgrammar-test). 35 | 36 | test/soql/ 37 | |-- simple_account.soql "Manually" created test cases 38 | |-- ... 39 | `-- snapshots/ "Snapshot-based" test cases 40 | |-- example-*.soql 41 | |-- example-*.soql.snap 42 | `-- ... 43 | 44 | The difference between manual vs. snapshot tests is that the latter are auto-generated and can be updated with command `vscode-tmgrammar-snap -u`. They are useful to quickly see the output of applying the grammar and catch regressions. 45 | 46 | The example-\* queries were taken from [Example SELECT clauses](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_examples.htm). 47 | 48 | ## Supported outputs 49 | 50 | - `grammars/apex.tmLanguage.cson` - for Atom 51 | - `grammars/apex.tmLanguage` - TextMate grammar (XML plist) 52 | - `grammars/soql.tmLanguage` - TextMate grammar (XML plist) for standalone SOQL files 53 | 54 | ## Releasing 55 | 56 | Merges to main on this repo with commits of type 'feat' or 'fix' get automatically published as a GitHub release and an NPM package through Github Actions. 57 | 58 | ## Attribution 59 | 60 | This repository was copied from [https://github.com/dotnet/csharp-tmLanguage](https://github.com/dotnet/csharp-tmLanguage) 61 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-parent-to-child.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, (SELECT LastName FROM Contacts) FROM Account; 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^ source.soql punctuation.parenthesis.open.apex 8 | # ^^^^^^ source.soql keyword.operator.query.select.apex 9 | # ^ source.soql 10 | # ^^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql punctuation.parenthesis.close.apex 16 | # ^ source.soql 17 | # ^^^^ source.soql keyword.operator.query.from.apex 18 | # ^ source.soql 19 | # ^^^^^^^ source.soql storage.type.apex 20 | # ^^ source.soql 21 | > 22 | >SELECT Account.Name, (SELECT Contact.LastName FROM Account.Contacts) FROM Account 23 | #^^^^^^ source.soql keyword.operator.query.select.apex 24 | # ^ source.soql 25 | # ^^^^^^^^^^^^ source.soql keyword.query.field.apex 26 | # ^ source.soql punctuation.separator.comma.apex 27 | # ^ source.soql 28 | # ^ source.soql punctuation.parenthesis.open.apex 29 | # ^^^^^^ source.soql keyword.operator.query.select.apex 30 | # ^ source.soql 31 | # ^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 32 | # ^ source.soql 33 | # ^^^^ source.soql keyword.operator.query.from.apex 34 | # ^ source.soql 35 | # ^^^^^^^^^^^^^^^^ source.soql storage.type.apex 36 | # ^ source.soql punctuation.parenthesis.close.apex 37 | # ^ source.soql 38 | # ^^^^ source.soql keyword.operator.query.from.apex 39 | # ^ source.soql 40 | # ^^^^^^^ source.soql storage.type.apex 41 | > -------------------------------------------------------------------------------- /docs/commit-guidelines.md: -------------------------------------------------------------------------------- 1 | # Commit Guidelines 2 | 3 | When working on a large project with multiple users, it's a good idea to follow 4 | a convention for committing in Git. A guide that we follow is 5 | https://chris.beams.io/posts/git-commit/. The purpose of this document is not to 6 | be nitpicky – instead, the goal is to help provide some guidelines on what is a 7 | good commit message. These guidelines are general, and apply to most projects 8 | that use Git. 9 | 10 | Here's a summary: 11 | 12 | - Separate subject from body with a blank line 13 | - Limit the subject line to 50 characters (this is a guide but not strictly 14 | enforced) 15 | - Capitalize the subject line 16 | - Do not end the subject line with a period 17 | - Use the imperative mood in the subject line 18 | - Wrap the body at 72 characters (this is not strictly 19 | enforced) 20 | - Use the body to explain what and why vs. how 21 | - Do not put internal bug numbers in the commit subject since it takes up the 22 | recommended 50 characters 23 | - The commit log is _not_ a diary - keep it short and relevant to the project, 24 | not to what a developer is thinking at that moment. Longer discussions can be 25 | had in the PRs. 26 | - Because we use squash and merge, ensure that the final squashed commit message 27 | makes sense. 28 | - No WIP markers in the subject 29 | - No DO-NOT-MERGE markers in the subject 30 | - No MyName/branchname in the subject 31 | 32 | Here's an example of a good and easy to read commit log showing only the subject 33 | (slightly modified from our git log output) 34 | 35 | ``` 36 | * 8d6a286 - (HEAD -> develop) Fix capitalization of package display name (#466) (3 weeks ago) 37 | * 96cb255 - Added feature request template (#462) (3 weeks ago) 38 | * 57abc05 - Add an event listener for changes to the sfdx-config file (#457) (3 weeks ago) 39 | * 5d5d7dc - Mark apex replay debugger as preview (#458) (3 weeks ago) 40 | * 8ace1d7 - Bump @salesforce/core to 1.5.1 (#456) (3 weeks ago) 41 | * fb7dda1 - Remove note about limitations of Live Share (#455) (3 weeks ago) 42 | * d07a21b - Send initialized event when logcontext is ready (#454) (4 weeks ago) 43 | * 42aaddd - Turn off logging will delete the traceflag (#437) (4 weeks ago) 44 | * 9adfcbb - Update UI Text (#453) (4 weeks ago) 45 | * 8dc4b75 - Merge branch 'release/v42.18.0' into develop (4 weeks ago) 46 | ... 47 | ``` 48 | 49 | Here's an example of a good and easy to read commit message (after everything has been squashed) 50 | 51 | ``` 52 | Ignore warnings and CLI update messages when using --json (#406) 53 | 54 | * Ignore warnings and CLI update messages when using --json 55 | * Remove integration tests for setting eslint 56 | * Clean up yarn lint 57 | * Remove test from processors as well 58 | 59 | @W-4485495@ 60 | ``` 61 | 62 | ### Committing 63 | 64 | 1. We enforce commit message format. 65 | 1. The commit message format that we expect is: `type: commit message`. Valid types are: feat, fix, improvement, docs, style, refactor, perf, test, build, ci, chore and revert. 66 | 1. Before commit and push, husky will run several hooks to ensure the commit message is in the correct format and that everything lints and compiles properly. 67 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-timerange.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT UserId, COUNT(Id) FROM LoginHistory WHERE LoginTime > 2010-09-20T22:16:30.000Z AND LoginTime < 2010-09-21T22:16:30.000Z GROUP BY UserId 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^ source.soql support.function.query.apex 8 | # ^ source.soql punctuation.parenthesis.open.apex 9 | # ^^ source.soql keyword.query.field.apex 10 | # ^ source.soql punctuation.parenthesis.close.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql 16 | # ^^^^^ source.soql keyword.operator.query.where.apex 17 | # ^ source.soql 18 | # ^^^^^^^^^ source.soql keyword.query.field.apex 19 | # ^ source.soql 20 | # ^ source.soql keyword.operator.relational.apex 21 | # ^ source.soql 22 | # ^^^^^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 23 | # ^ source.soql 24 | # ^^^ source.soql keyword.operator.query.apex 25 | # ^ source.soql 26 | # ^^^^^^^^^ source.soql keyword.query.field.apex 27 | # ^ source.soql 28 | # ^ source.soql keyword.operator.relational.apex 29 | # ^ source.soql 30 | # ^^^^^^^^^^^^^^^^^^^^^^^^ source.soql constant.numeric.datetime.apex 31 | # ^ source.soql 32 | # ^^^^^^^^ source.soql keyword.operator.query.apex 33 | # ^ source.soql 34 | # ^^^^^^ source.soql keyword.query.field.apex 35 | > -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/InitializerBlockSyntax_PR73.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #73: Initializer block syntax highlighting 3 | * 4 | * This class demonstrates initialization blocks in nested classes with method calls. 5 | * Issue #4920: Initializer block syntax highlighting should match method body highlighting. 6 | */ 7 | public class InitializerBlockSyntax_PR73 { 8 | 9 | public class NoneDAGException extends Exception { 10 | // Initializer - string literal should be consistently highlighted 11 | // Issue: String highlighting is broken in initializer blocks 12 | { 13 | this.setMessage('Object graph should be a Directed Acyclic Graph.'); 14 | this.setMessage('Simple string'); 15 | this.setMessage('String with "quotes" inside'); 16 | System.debug('Debug message in initializer'); 17 | } 18 | 19 | // Sample method for comparison - same strings highlighted correctly here 20 | public void anotherMethod() { 21 | this.setMessage('Object graph should be a Directed Acyclic Graph.'); 22 | this.setMessage('Simple string'); 23 | this.setMessage('String with "quotes" inside'); 24 | System.debug('Debug message in method'); 25 | } 26 | } 27 | 28 | public class ExampleWithInitializer { 29 | private String message; 30 | 31 | // Empty initializer 32 | { 33 | } 34 | 35 | // Initializer with multiple statements 36 | { 37 | Integer x = 5; 38 | String msg = 'test'; 39 | message = msg; 40 | System.debug(message); 41 | } 42 | } 43 | 44 | public class StaticInitializerExample { 45 | private static Integer counter; 46 | 47 | // Note: Apex does NOT support static initialization blocks like Java 48 | // This tests grammar handling 49 | static { 50 | counter = 0; 51 | } 52 | } 53 | 54 | /** 55 | * Test case: String literals in initializer blocks should have consistent highlighting. 56 | * Issue: Strings in initializer blocks show inconsistent/incorrect highlighting 57 | * compared to the same strings in method bodies. 58 | */ 59 | public class StringHighlightingInInitializer { 60 | private String message; 61 | 62 | // Initializer block with various string scenarios 63 | { 64 | // All these strings should be highlighted consistently 65 | message = 'Initializer string'; 66 | message = 'String with keywords like Object and Graph'; 67 | message = 'String with numbers 123 and symbols !@#'; 68 | this.setMessage('Method call with string parameter'); 69 | System.debug('Debug: initializer block string'); 70 | } 71 | 72 | // Method body for comparison - same strings work correctly here 73 | public void compareMethod() { 74 | message = 'Initializer string'; 75 | message = 'String with keywords like Object and Graph'; 76 | message = 'String with numbers 123 and symbols !@#'; 77 | this.setMessage('Method call with string parameter'); 78 | System.debug('Debug: method body string'); 79 | } 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /test/interface.test.ts: -------------------------------------------------------------------------------- 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('Interfaces', () => { 16 | it('simple interface', async () => { 17 | const input = `interface IFoo { }`; 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.Keywords.Interface, 22 | Token.Identifiers.InterfaceName('IFoo'), 23 | Token.Punctuation.OpenBrace, 24 | Token.Punctuation.CloseBrace, 25 | ]); 26 | }); 27 | 28 | it('interface inheritance', async () => { 29 | const input = ` 30 | interface IFoo { } 31 | interface IBar extends IFoo { } 32 | `; 33 | 34 | const tokens = await tokenize(input); 35 | 36 | tokens.should.deep.equal([ 37 | Token.Keywords.Interface, 38 | Token.Identifiers.InterfaceName('IFoo'), 39 | Token.Punctuation.OpenBrace, 40 | Token.Punctuation.CloseBrace, 41 | Token.Keywords.Interface, 42 | Token.Identifiers.InterfaceName('IBar'), 43 | Token.Keywords.Extends, 44 | Token.Identifiers.ExtendsName('IFoo'), 45 | Token.Punctuation.OpenBrace, 46 | Token.Punctuation.CloseBrace, 47 | ]); 48 | }); 49 | 50 | it('generic interface', async () => { 51 | const input = `interface IFoo { }`; 52 | const tokens = await tokenize(input); 53 | 54 | tokens.should.deep.equal([ 55 | Token.Keywords.Interface, 56 | Token.Identifiers.InterfaceName('IFoo'), 57 | Token.Punctuation.TypeParameters.Begin, 58 | Token.Identifiers.TypeParameterName('T1'), 59 | Token.Punctuation.Comma, 60 | Token.Identifiers.TypeParameterName('T2'), 61 | Token.Punctuation.TypeParameters.End, 62 | Token.Punctuation.OpenBrace, 63 | Token.Punctuation.CloseBrace, 64 | ]); 65 | }); 66 | 67 | it('interface extends another interface', async () => { 68 | const input = `public interface MyInterface2 extends MyInterface {}`; 69 | const tokens = await tokenize(input); 70 | 71 | tokens.should.deep.equal([ 72 | Token.Keywords.Modifiers.Public, 73 | Token.Keywords.Interface, 74 | Token.Identifiers.InterfaceName('MyInterface2'), 75 | Token.Keywords.Extends, 76 | Token.Identifiers.ExtendsName('MyInterface'), 77 | Token.Punctuation.OpenBrace, 78 | Token.Punctuation.CloseBrace, 79 | ]); 80 | }); 81 | 82 | it('interface extends namespace-qualified type (issue #50)', async () => { 83 | const input = Input.FromText( 84 | `interface MyInterface extends System.IComparable {}` 85 | ); 86 | const tokens = await tokenize(input); 87 | 88 | tokens.should.deep.equal([ 89 | Token.Keywords.Interface, 90 | Token.Identifiers.InterfaceName('MyInterface'), 91 | Token.Keywords.Extends, 92 | Token.Support.Class.System, 93 | Token.Punctuation.Accessor, 94 | Token.Support.Class.TypeText('IComparable'), 95 | Token.Punctuation.OpenBrace, 96 | Token.Punctuation.CloseBrace, 97 | ]); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-WHERE.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, (SELECT LastName FROM Contacts WHERE CreatedBy.Alias = 'x') FROM Account WHERE Industry = 'media' 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^ source.soql punctuation.parenthesis.open.apex 8 | # ^^^^^^ source.soql keyword.operator.query.select.apex 9 | # ^ source.soql 10 | # ^^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^^^^ source.soql storage.type.apex 15 | # ^ source.soql 16 | # ^^^^^ source.soql keyword.operator.query.where.apex 17 | # ^ source.soql 18 | # ^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 19 | # ^ source.soql 20 | # ^ source.soql keyword.operator.assignment.apex 21 | # ^ source.soql 22 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 23 | # ^ source.soql string.quoted.single.apex 24 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 25 | # ^ source.soql punctuation.parenthesis.close.apex 26 | # ^ source.soql 27 | # ^^^^ source.soql keyword.operator.query.from.apex 28 | # ^ source.soql 29 | # ^^^^^^^ source.soql storage.type.apex 30 | # ^ source.soql 31 | # ^^^^^ source.soql keyword.operator.query.where.apex 32 | # ^ source.soql 33 | # ^^^^^^^^ source.soql keyword.query.field.apex 34 | # ^ source.soql 35 | # ^ source.soql keyword.operator.assignment.apex 36 | # ^ source.soql 37 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 38 | # ^^^^^ source.soql string.quoted.single.apex 39 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 40 | > -------------------------------------------------------------------------------- /test/repros/force-app/main/default/classes/SwitchWhenBraceMatching_PR74_PR75.cls: -------------------------------------------------------------------------------- 1 | /** 2 | * PR #74/#75: Switch/when statement syntax highlighting and brace matching 3 | * 4 | * This class demonstrates switch statements with when clauses. 5 | * Issue #2134: Switch/when statement syntax highlighting and brace matching. 6 | */ 7 | public class SwitchWhenBraceMatching_PR74_PR75 { 8 | 9 | /** 10 | * Test case matching issue #2134: 11 | * - Strings in when clause should be consistently syntax highlighted 12 | * - Bracket matching should match up pairs 13 | * - Brace matching issues when brace follows when clause 14 | */ 15 | public void issue2134_switchWhenBraceMatching() { 16 | String param = 'test'; 17 | switch on param { 18 | when 'A' { 19 | System.debug('when A'); 20 | } 21 | when 'B' { 22 | System.debug('when B'); 23 | } 24 | when 'C' 25 | { 26 | // Brace on new line after when clause 27 | System.debug('when C'); 28 | } 29 | when 'D'{ 30 | // Brace immediately after when clause without space 31 | System.debug('when D'); 32 | } 33 | when else { 34 | System.debug('else'); 35 | } 36 | } 37 | } 38 | 39 | public void simpleSwitchOnString() { 40 | String param = 'A'; 41 | switch on (param) { 42 | when 'A' { 43 | System.debug('test'); 44 | } 45 | when else { 46 | callExternalMethod(); 47 | } 48 | } 49 | } 50 | 51 | public void switchWithMultipleStringValues() { 52 | String locale = 'de-CH'; 53 | switch on locale { 54 | when 'de-CH' { 55 | System.debug('German Switzerland'); 56 | } 57 | when 'fr-CH' { 58 | System.debug('French Switzerland'); 59 | } 60 | when else { 61 | System.debug('Other locale'); 62 | } 63 | } 64 | } 65 | 66 | public void switchOnInteger() { 67 | Integer i = 5; 68 | switch on i { 69 | when 2, 3, 4 { 70 | System.debug('when block 2 and 3 and 4'); 71 | } 72 | when 7 { 73 | System.debug('when block 7'); 74 | } 75 | when else { 76 | // @TODO. 77 | } 78 | } 79 | } 80 | 81 | public void switchOnSObject() { 82 | SObject sobject = new Account(); 83 | switch on sobject { 84 | when Account a { 85 | System.debug('account ' + a); 86 | } 87 | when null { 88 | System.debug('null'); 89 | } 90 | when else { 91 | System.debug('default'); 92 | } 93 | } 94 | } 95 | 96 | public void switchOnMethodResult() { 97 | switch on someInteger(getValue()) { 98 | when 2, 3, 4 { 99 | System.debug('when block 2 and 3 and 4'); 100 | } 101 | when 7 { 102 | System.debug('when block 7'); 103 | } 104 | when else { 105 | // @TODO. 106 | } 107 | } 108 | } 109 | 110 | public void switchWithSafeNavigator() { 111 | MyClass obj = new MyClass(); 112 | switch on (obj?.param) { 113 | when 'A' { 114 | System.debug('test'); 115 | } 116 | when else { 117 | callExternalMethod(); 118 | } 119 | } 120 | } 121 | 122 | private Integer someInteger(Integer val) { 123 | return val; 124 | } 125 | 126 | private Integer getValue() { 127 | return 5; 128 | } 129 | 130 | private void callExternalMethod() { 131 | System.debug('external'); 132 | } 133 | 134 | private class MyClass { 135 | public String param; 136 | } 137 | } 138 | 139 | -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship_TYPEOF.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT TYPEOF What WHEN Account THEN Phone, NumberOfEmployees WHEN Opportunity THEN Amount, CloseDate ELSE Name, Email END FROM Event 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^^^ source.soql keyword.operator.query.apex 5 | # ^ source.soql 6 | # ^^^^ source.soql keyword.query.field.apex 7 | # ^ source.soql 8 | # ^^^^ source.soql keyword.query.field.apex 9 | # ^ source.soql 10 | # ^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.query.field.apex 13 | # ^ source.soql 14 | # ^^^^^ source.soql keyword.query.field.apex 15 | # ^ source.soql punctuation.separator.comma.apex 16 | # ^ source.soql 17 | # ^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 18 | # ^ source.soql 19 | # ^^^^ source.soql keyword.query.field.apex 20 | # ^ source.soql 21 | # ^^^^^^^^^^^ source.soql keyword.query.field.apex 22 | # ^ source.soql 23 | # ^^^^ source.soql keyword.query.field.apex 24 | # ^ source.soql 25 | # ^^^^^^ source.soql keyword.query.field.apex 26 | # ^ source.soql punctuation.separator.comma.apex 27 | # ^ source.soql 28 | # ^^^^^^^^^ source.soql keyword.query.field.apex 29 | # ^ source.soql 30 | # ^^^^ source.soql keyword.query.field.apex 31 | # ^ source.soql 32 | # ^^^^ source.soql keyword.query.field.apex 33 | # ^ source.soql punctuation.separator.comma.apex 34 | # ^ source.soql 35 | # ^^^^^ source.soql keyword.query.field.apex 36 | # ^ source.soql 37 | # ^^^ source.soql keyword.query.field.apex 38 | # ^ source.soql 39 | # ^^^^ source.soql keyword.operator.query.from.apex 40 | # ^ source.soql 41 | # ^^^^^ source.soql storage.type.apex 42 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-polymorphic-key.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Id, Owner.Name FROM Task WHERE Owner.FirstName like 'B%'; 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^^^^^^^^^^ source.soql keyword.query.field.apex 8 | # ^ source.soql 9 | # ^^^^ source.soql keyword.operator.query.from.apex 10 | # ^ source.soql 11 | # ^^^^ source.soql storage.type.apex 12 | # ^ source.soql 13 | # ^^^^^ source.soql keyword.operator.query.where.apex 14 | # ^ source.soql 15 | # ^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 16 | # ^ source.soql 17 | # ^^^^ source.soql keyword.query.field.apex 18 | # ^ source.soql 19 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 20 | # ^^ source.soql string.quoted.single.apex 21 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 22 | # ^^ source.soql 23 | > 24 | >SELECT Id, Who.FirstName, Who.LastName FROM Task WHERE Owner.FirstName LIKE 'B%'; 25 | #^^^^^^ source.soql keyword.operator.query.select.apex 26 | # ^ source.soql 27 | # ^^ source.soql keyword.query.field.apex 28 | # ^ source.soql punctuation.separator.comma.apex 29 | # ^ source.soql 30 | # ^^^^^^^^^^^^^ source.soql keyword.query.field.apex 31 | # ^ source.soql punctuation.separator.comma.apex 32 | # ^ source.soql 33 | # ^^^^^^^^^^^^ source.soql keyword.query.field.apex 34 | # ^ source.soql 35 | # ^^^^ source.soql keyword.operator.query.from.apex 36 | # ^ source.soql 37 | # ^^^^ source.soql storage.type.apex 38 | # ^ source.soql 39 | # ^^^^^ source.soql keyword.operator.query.where.apex 40 | # ^ source.soql 41 | # ^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 42 | # ^ source.soql 43 | # ^^^^ source.soql keyword.operator.query.apex 44 | # ^ source.soql 45 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.begin.apex 46 | # ^^ source.soql string.quoted.single.apex 47 | # ^ source.soql string.quoted.single.apex punctuation.definition.string.end.apex 48 | # ^^ source.soql 49 | > 50 | >SELECT Id, What.Name FROM Event; 51 | #^^^^^^ source.soql keyword.operator.query.select.apex 52 | # ^ source.soql 53 | # ^^ source.soql keyword.query.field.apex 54 | # ^ source.soql punctuation.separator.comma.apex 55 | # ^ source.soql 56 | # ^^^^^^^^^ source.soql keyword.query.field.apex 57 | # ^ source.soql 58 | # ^^^^ source.soql keyword.operator.query.from.apex 59 | # ^ source.soql 60 | # ^^^^^ source.soql storage.type.apex 61 | # ^^ source.soql 62 | > -------------------------------------------------------------------------------- /test/soql/snapshots/example-relationship-with-aggregate.soql.snap: -------------------------------------------------------------------------------- 1 | >SELECT Name, (SELECT CreatedBy.Name FROM Notes) FROM Account; 2 | #^^^^^^ source.soql keyword.operator.query.select.apex 3 | # ^ source.soql 4 | # ^^^^ source.soql keyword.query.field.apex 5 | # ^ source.soql punctuation.separator.comma.apex 6 | # ^ source.soql 7 | # ^ source.soql punctuation.parenthesis.open.apex 8 | # ^^^^^^ source.soql keyword.operator.query.select.apex 9 | # ^ source.soql 10 | # ^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 11 | # ^ source.soql 12 | # ^^^^ source.soql keyword.operator.query.from.apex 13 | # ^ source.soql 14 | # ^^^^^ source.soql storage.type.apex 15 | # ^ source.soql punctuation.parenthesis.close.apex 16 | # ^ source.soql 17 | # ^^^^ source.soql keyword.operator.query.from.apex 18 | # ^ source.soql 19 | # ^^^^^^^ source.soql storage.type.apex 20 | # ^^ source.soql 21 | > 22 | >SELECT Amount, Id, Name, (SELECT Quantity, ListPrice, PricebookEntry.UnitPrice, PricebookEntry.Name FROM OpportunityLineItems) FROM Opportunity 23 | #^^^^^^ source.soql keyword.operator.query.select.apex 24 | # ^ source.soql 25 | # ^^^^^^ source.soql keyword.query.field.apex 26 | # ^ source.soql punctuation.separator.comma.apex 27 | # ^ source.soql 28 | # ^^ source.soql keyword.query.field.apex 29 | # ^ source.soql punctuation.separator.comma.apex 30 | # ^ source.soql 31 | # ^^^^ source.soql keyword.query.field.apex 32 | # ^ source.soql punctuation.separator.comma.apex 33 | # ^ source.soql 34 | # ^ source.soql punctuation.parenthesis.open.apex 35 | # ^^^^^^ source.soql keyword.operator.query.select.apex 36 | # ^ source.soql 37 | # ^^^^^^^^ source.soql keyword.query.field.apex 38 | # ^ source.soql punctuation.separator.comma.apex 39 | # ^ source.soql 40 | # ^^^^^^^^^ source.soql keyword.query.field.apex 41 | # ^ source.soql punctuation.separator.comma.apex 42 | # ^ source.soql 43 | # ^^^^^^^^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 44 | # ^ source.soql punctuation.separator.comma.apex 45 | # ^ source.soql 46 | # ^^^^^^^^^^^^^^^^^^^ source.soql keyword.query.field.apex 47 | # ^ source.soql 48 | # ^^^^ source.soql keyword.operator.query.from.apex 49 | # ^ source.soql 50 | # ^^^^^^^^^^^^^^^^^^^^ source.soql storage.type.apex 51 | # ^ source.soql punctuation.parenthesis.close.apex 52 | # ^ source.soql 53 | # ^^^^ source.soql keyword.operator.query.from.apex 54 | # ^ source.soql 55 | # ^^^^^^^^^^^ source.soql storage.type.apex 56 | > -------------------------------------------------------------------------------- /test/type-name.test.ts: -------------------------------------------------------------------------------- 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('Type names', () => { 16 | it('built-in type - object', async () => { 17 | const input = Input.InClass(`Object x;`); 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.PrimitiveType.Object, 22 | Token.Identifiers.FieldName('x'), 23 | Token.Punctuation.Semicolon, 24 | ]); 25 | }); 26 | 27 | it('generic type - List', async () => { 28 | const input = Input.InClass(`List x;`); 29 | const tokens = await tokenize(input); 30 | 31 | tokens.should.deep.equal([ 32 | Token.Type('List'), 33 | Token.Punctuation.TypeParameters.Begin, 34 | Token.PrimitiveType.Integer, 35 | Token.Punctuation.TypeParameters.End, 36 | Token.Identifiers.FieldName('x'), 37 | Token.Punctuation.Semicolon, 38 | ]); 39 | }); 40 | 41 | it('generic type with multiple parameters - Dictionary', async () => { 42 | const input = Input.InClass(`Dictionary x;`); 43 | const tokens = await tokenize(input); 44 | 45 | tokens.should.deep.equal([ 46 | Token.Type('Dictionary'), 47 | Token.Punctuation.TypeParameters.Begin, 48 | Token.PrimitiveType.Integer, 49 | Token.Punctuation.Comma, 50 | Token.PrimitiveType.Integer, 51 | Token.Punctuation.TypeParameters.End, 52 | Token.Identifiers.FieldName('x'), 53 | Token.Punctuation.Semicolon, 54 | ]); 55 | }); 56 | 57 | it('qualified generic type - System.Collections.Generic.List', async () => { 58 | const input = Input.InClass( 59 | `System.Collections.Generic.List x;` 60 | ); 61 | const tokens = await tokenize(input); 62 | 63 | tokens.should.deep.equal([ 64 | Token.Support.Class.System, 65 | Token.Punctuation.Accessor, 66 | Token.Support.Class.TypeText('Collections'), 67 | Token.Punctuation.Accessor, 68 | Token.Support.Class.TypeText('Generic'), 69 | Token.Punctuation.Accessor, 70 | Token.Support.Class.TypeText('List'), 71 | Token.Punctuation.TypeParameters.Begin, 72 | Token.PrimitiveType.Integer, 73 | Token.Punctuation.TypeParameters.End, 74 | Token.Identifiers.FieldName('x'), 75 | Token.Punctuation.Semicolon, 76 | ]); 77 | }); 78 | 79 | it('nullable type - int', async () => { 80 | const input = Input.InClass(`Integer x;`); 81 | const tokens = await tokenize(input); 82 | 83 | tokens.should.deep.equal([ 84 | Token.PrimitiveType.Integer, 85 | Token.Identifiers.FieldName('x'), 86 | Token.Punctuation.Semicolon, 87 | ]); 88 | }); 89 | 90 | it('Id type (lowercase d) - Apex is case-insensitive', async () => { 91 | const input = Input.InClass(`Id recordId;`); 92 | const tokens = await tokenize(input); 93 | 94 | tokens.should.deep.equal([ 95 | Token.PrimitiveType.ID, 96 | Token.Identifiers.FieldName('recordId'), 97 | Token.Punctuation.Semicolon, 98 | ]); 99 | }); 100 | 101 | it('ID type (uppercase D) - Apex is case-insensitive', async () => { 102 | const input = Input.InClass(`ID recordId;`); 103 | const tokens = await tokenize(input); 104 | 105 | tokens.should.deep.equal([ 106 | { text: 'ID', type: 'keyword.type.apex' }, 107 | Token.Identifiers.FieldName('recordId'), 108 | Token.Punctuation.Semicolon, 109 | ]); 110 | }); 111 | 112 | it('Id in generic type parameter', async () => { 113 | const input = Input.InClass(`Map accounts;`); 114 | const tokens = await tokenize(input); 115 | 116 | tokens.should.deep.equal([ 117 | Token.Type('Map'), 118 | Token.Punctuation.TypeParameters.Begin, 119 | Token.PrimitiveType.ID, 120 | Token.Punctuation.Comma, 121 | Token.Type('Account'), 122 | Token.Punctuation.TypeParameters.End, 123 | Token.Identifiers.FieldName('accounts'), 124 | Token.Punctuation.Semicolon, 125 | ]); 126 | }); 127 | 128 | it('ID in generic type parameter', async () => { 129 | const input = Input.InClass(`Map accounts;`); 130 | const tokens = await tokenize(input); 131 | 132 | tokens.should.deep.equal([ 133 | Token.Type('Map'), 134 | Token.Punctuation.TypeParameters.Begin, 135 | { text: 'ID', type: 'keyword.type.apex' }, 136 | Token.Punctuation.Comma, 137 | Token.Type('Account'), 138 | Token.Punctuation.TypeParameters.End, 139 | Token.Identifiers.FieldName('accounts'), 140 | Token.Punctuation.Semicolon, 141 | ]); 142 | }); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Salesforce Open Source Community Code of Conduct 2 | 3 | ## About the Code of Conduct 4 | 5 | Equality is a core value at Salesforce. We believe a diverse and inclusive 6 | community fosters innovation and creativity, and are committed to building a 7 | culture where everyone feels included. 8 | 9 | Salesforce open-source projects are committed to providing a friendly, safe, and 10 | welcoming environment for all, regardless of gender identity and expression, 11 | sexual orientation, disability, physical appearance, body size, ethnicity, nationality, 12 | race, age, religion, level of experience, education, socioeconomic status, or 13 | other similar personal characteristics. 14 | 15 | The goal of this code of conduct is to specify a baseline standard of behavior so 16 | that people with different social values and communication styles can work 17 | together effectively, productively, and respectfully in our open source community. 18 | It also establishes a mechanism for reporting issues and resolving conflicts. 19 | 20 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior 21 | in a Salesforce open-source project may be reported by contacting the Salesforce 22 | Open Source Conduct Committee at ossconduct@salesforce.com. 23 | 24 | ## Our Pledge 25 | 26 | In the interest of fostering an open and welcoming environment, we as 27 | contributors and maintainers pledge to making participation in our project and 28 | our community a harassment-free experience for everyone, regardless of gender 29 | identity and expression, sexual orientation, disability, physical appearance, 30 | body size, ethnicity, nationality, race, age, religion, level of experience, education, 31 | socioeconomic status, or other similar personal characteristics. 32 | 33 | ## Our Standards 34 | 35 | Examples of behavior that contributes to creating a positive environment 36 | include: 37 | 38 | - Using welcoming and inclusive language 39 | - Being respectful of differing viewpoints and experiences 40 | - Gracefully accepting constructive criticism 41 | - Focusing on what is best for the community 42 | - Showing empathy toward other community members 43 | 44 | Examples of unacceptable behavior by participants include: 45 | 46 | - The use of sexualized language or imagery and unwelcome sexual attention or 47 | advances 48 | - Personal attacks, insulting/derogatory comments, or trolling 49 | - Public or private harassment 50 | - Publishing, or threatening to publish, others' private information—such as 51 | a physical or electronic address—without explicit permission 52 | - Other conduct which could reasonably be considered inappropriate in a 53 | professional setting 54 | - Advocating for or encouraging any of the above behaviors 55 | 56 | ## Our Responsibilities 57 | 58 | Project maintainers are responsible for clarifying the standards of acceptable 59 | behavior and are expected to take appropriate and fair corrective action in 60 | response to any instances of unacceptable behavior. 61 | 62 | Project maintainers have the right and responsibility to remove, edit, or 63 | reject comments, commits, code, wiki edits, issues, and other contributions 64 | that are not aligned with this Code of Conduct, or to ban temporarily or 65 | permanently any contributor for other behaviors that they deem inappropriate, 66 | threatening, offensive, or harmful. 67 | 68 | ## Scope 69 | 70 | This Code of Conduct applies both within project spaces and in public spaces 71 | when an individual is representing the project or its community. Examples of 72 | representing a project or community include using an official project email 73 | address, posting via an official social media account, or acting as an appointed 74 | representative at an online or offline event. Representation of a project may be 75 | further defined and clarified by project maintainers. 76 | 77 | ## Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported by contacting the Salesforce Open Source Conduct Committee 81 | at ossconduct@salesforce.com. All complaints will be reviewed and investigated 82 | and will result in a response that is deemed necessary and appropriate to the 83 | circumstances. The committee is obligated to maintain confidentiality with 84 | regard to the reporter of an incident. Further details of specific enforcement 85 | policies may be posted separately. 86 | 87 | Project maintainers who do not follow or enforce the Code of Conduct in good 88 | faith may face temporary or permanent repercussions as determined by other 89 | members of the project's leadership and the Salesforce Open Source Conduct 90 | Committee. 91 | 92 | ## Attribution 93 | 94 | This Code of Conduct is adapted from the [Contributor Covenant][contributor-covenant-home], 95 | version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. 96 | It includes adaptions and additions from [Go Community Code of Conduct][golang-coc], 97 | [CNCF Code of Conduct][cncf-coc], and [Microsoft Open Source Code of Conduct][microsoft-coc]. 98 | 99 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License][cc-by-3-us]. 100 | 101 | [contributor-covenant-home]: https://www.contributor-covenant.org 'https://www.contributor-covenant.org/' 102 | [golang-coc]: https://golang.org/conduct 103 | [cncf-coc]: https://github.com/cncf/foundation/blob/master/code-of-conduct.md 104 | [microsoft-coc]: https://opensource.microsoft.com/codeofconduct/ 105 | [cc-by-3-us]: https://creativecommons.org/licenses/by/3.0/us/ 106 | -------------------------------------------------------------------------------- /test/javadoc.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2020 Salesforce. All rights reserved. 3 | * See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { should } from 'chai'; 7 | import { tokenize, Input, Token } from './utils/tokenize'; 8 | 9 | describe('Grammar', () => { 10 | before(() => { 11 | should(); 12 | }); 13 | describe('JavaDoc', () => { 14 | it('multi-line comment with no content', async () => { 15 | const input = `/***********/`; 16 | const tokens = await tokenize(input); 17 | 18 | tokens.should.deep.equal([ 19 | Token.Comment.MultiLine.JavaDocStart, 20 | Token.Comment.MultiLine.JavaDocText('********'), 21 | Token.Comment.MultiLine.End, 22 | ]); 23 | }); 24 | 25 | it('multi-line java doc comment', async () => { 26 | const input = Input.FromText(` 27 | /** 28 | * foo 29 | */ 30 | `); 31 | const tokens = await tokenize(input); 32 | 33 | tokens.should.deep.equal([ 34 | Token.Comment.MultiLine.JavaDocStart, 35 | Token.Comment.MultiLine.JavaDocText('* foo'), 36 | Token.Comment.MultiLine.End, 37 | ]); 38 | }); 39 | 40 | it('multi-line comment with content', async () => { 41 | const input = Input.FromText(` 42 | /*************/ 43 | /***** foo ***/ 44 | /*************/ 45 | `); 46 | const tokens = await tokenize(input); 47 | 48 | tokens.should.deep.equal([ 49 | Token.Comment.MultiLine.JavaDocStart, 50 | Token.Comment.MultiLine.JavaDocText('**********'), 51 | Token.Comment.MultiLine.End, 52 | Token.Comment.MultiLine.JavaDocStart, 53 | Token.Comment.MultiLine.JavaDocText('*** foo **'), 54 | Token.Comment.MultiLine.End, 55 | Token.Comment.MultiLine.JavaDocStart, 56 | Token.Comment.MultiLine.JavaDocText('**********'), 57 | Token.Comment.MultiLine.End, 58 | ]); 59 | }); 60 | 61 | it('multi-line comment with content', async () => { 62 | const input = Input.FromText(` 63 | /** 64 | * @return null 65 | */ 66 | `); 67 | const tokens = await tokenize(input); 68 | 69 | tokens.should.deep.equal([ 70 | Token.Comment.MultiLine.JavaDocStart, 71 | Token.Comment.MultiLine.JavaDocText('* '), 72 | Token.Comment.MultiLine.JavaDocKeyword('@return'), 73 | Token.Comment.MultiLine.JavaDocText(' null'), 74 | Token.Comment.MultiLine.End, 75 | ]); 76 | }); 77 | 78 | it('multi-line comment with annotations', async () => { 79 | const input = Input.FromText(` 80 | /** 81 | * Interactively reinvent high-payoff convergence 82 | * with professional process improvements. 83 | *

84 | * Continually initiate client-centric mindshare 85 | * without innovative value. Compellingly formulate 86 | * sustainable e-business via revolutionary supply chains. 87 | * 88 | * @param url base location url 89 | * @param name the name of the asset 90 | * @return the full URL of the asset 91 | * 92 | * @throws myCustomException 93 | * @author Eduardo Mora em@example.com 94 | */`); 95 | const tokens = await tokenize(input); 96 | 97 | tokens.should.deep.equal([ 98 | Token.Comment.MultiLine.JavaDocStart, 99 | Token.Comment.MultiLine.JavaDocText( 100 | ' * Interactively reinvent high-payoff convergence' 101 | ), 102 | Token.Comment.MultiLine.JavaDocText( 103 | ' * with professional process improvements.' 104 | ), 105 | Token.Comment.MultiLine.JavaDocText(' *

'), 106 | Token.Comment.MultiLine.JavaDocText( 107 | ' * Continually initiate client-centric mindshare' 108 | ), 109 | Token.Comment.MultiLine.JavaDocText( 110 | ' * without innovative value. Compellingly formulate ' 111 | ), 112 | Token.Comment.MultiLine.JavaDocText( 113 | ' * sustainable e-business via revolutionary supply chains.' 114 | ), 115 | Token.Comment.MultiLine.JavaDocText(' *'), 116 | Token.Comment.MultiLine.JavaDocText(' * '), 117 | Token.Comment.MultiLine.JavaDocKeyword('@param'), 118 | Token.Comment.MultiLine.JavaDocText(' '), 119 | Token.Identifiers.ParameterName('url'), 120 | Token.Comment.MultiLine.JavaDocText(' base location url'), 121 | Token.Comment.MultiLine.JavaDocText(' * '), 122 | Token.Comment.MultiLine.JavaDocKeyword('@param'), 123 | Token.Comment.MultiLine.JavaDocText(' '), 124 | Token.Identifiers.ParameterName('name'), 125 | Token.Comment.MultiLine.JavaDocText(' the name of the asset'), 126 | Token.Comment.MultiLine.JavaDocText(' * '), 127 | Token.Comment.MultiLine.JavaDocKeyword('@return'), 128 | Token.Comment.MultiLine.JavaDocText(' the full URL of the asset'), 129 | Token.Comment.MultiLine.JavaDocText(' * '), 130 | Token.Comment.MultiLine.JavaDocText(' * '), 131 | Token.Comment.MultiLine.JavaDocKeyword('@throws'), 132 | Token.Comment.MultiLine.JavaDocText(' '), 133 | Token.Identifiers.ClassName('myCustomException'), 134 | Token.Comment.MultiLine.JavaDocText(' * '), 135 | Token.Comment.MultiLine.JavaDocKeyword('@author'), 136 | Token.Comment.MultiLine.JavaDocText(' Eduardo Mora em@example.com'), 137 | Token.Comment.MultiLine.JavaDocText(' '), 138 | Token.Comment.MultiLine.End, 139 | ]); 140 | }); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /test/field.test.ts: -------------------------------------------------------------------------------- 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('Field', () => { 16 | it('declaration', async () => { 17 | const input = Input.InClass(` 18 | private List _field; 19 | private List field; 20 | private List field123;`); 21 | 22 | const tokens = await tokenize(input); 23 | 24 | tokens.should.deep.equal([ 25 | Token.Keywords.Modifiers.Private, 26 | Token.Type('List'), 27 | Token.Identifiers.FieldName('_field'), 28 | Token.Punctuation.Semicolon, 29 | 30 | Token.Keywords.Modifiers.Private, 31 | Token.Type('List'), 32 | Token.Identifiers.FieldName('field'), 33 | Token.Punctuation.Semicolon, 34 | 35 | Token.Keywords.Modifiers.Private, 36 | Token.Type('List'), 37 | Token.Identifiers.FieldName('field123'), 38 | Token.Punctuation.Semicolon, 39 | ]); 40 | }); 41 | 42 | it('generic', async () => { 43 | const input = Input.InClass( 44 | `private Dictionary< List, Dictionary> _field;` 45 | ); 46 | const tokens = await tokenize(input); 47 | 48 | tokens.should.deep.equal([ 49 | Token.Keywords.Modifiers.Private, 50 | Token.Type('Dictionary'), 51 | Token.Punctuation.TypeParameters.Begin, 52 | Token.Type('List'), 53 | Token.Punctuation.TypeParameters.Begin, 54 | Token.Type('T'), 55 | Token.Punctuation.TypeParameters.End, 56 | Token.Punctuation.Comma, 57 | Token.Type('Dictionary'), 58 | Token.Punctuation.TypeParameters.Begin, 59 | Token.Type('T'), 60 | Token.Punctuation.Comma, 61 | Token.Type('D'), 62 | Token.Punctuation.TypeParameters.End, 63 | Token.Punctuation.TypeParameters.End, 64 | Token.Identifiers.FieldName('_field'), 65 | Token.Punctuation.Semicolon, 66 | ]); 67 | }); 68 | 69 | it('types', async () => { 70 | const input = Input.InClass(` 71 | String field123; 72 | String[] field123;`); 73 | 74 | const tokens = await tokenize(input); 75 | 76 | tokens.should.deep.equal([ 77 | Token.PrimitiveType.String, 78 | Token.Identifiers.FieldName('field123'), 79 | Token.Punctuation.Semicolon, 80 | 81 | Token.PrimitiveType.String, 82 | Token.Punctuation.OpenBracket, 83 | Token.Punctuation.CloseBracket, 84 | Token.Identifiers.FieldName('field123'), 85 | Token.Punctuation.Semicolon, 86 | ]); 87 | }); 88 | 89 | it('assignment', async () => { 90 | const input = Input.InClass(` 91 | private String field = 'hello'; 92 | Boolean field = true;`); 93 | 94 | let tokens = await tokenize(input); 95 | 96 | tokens.should.deep.equal([ 97 | Token.Keywords.Modifiers.Private, 98 | Token.PrimitiveType.String, 99 | Token.Identifiers.FieldName('field'), 100 | Token.Operators.Assignment, 101 | Token.Punctuation.String.Begin, 102 | Token.Literals.String('hello'), 103 | Token.Punctuation.String.End, 104 | Token.Punctuation.Semicolon, 105 | 106 | Token.PrimitiveType.Boolean, 107 | Token.Identifiers.FieldName('field'), 108 | Token.Operators.Assignment, 109 | Token.Literals.Boolean.True, 110 | Token.Punctuation.Semicolon, 111 | ]); 112 | }); 113 | 114 | it('declaration with multiple declarators', async () => { 115 | const input = Input.InClass(`Integer x = 19, y = 23, z = 42;`); 116 | const tokens = await tokenize(input); 117 | 118 | tokens.should.deep.equal([ 119 | Token.PrimitiveType.Integer, 120 | Token.Identifiers.FieldName('x'), 121 | Token.Operators.Assignment, 122 | Token.Literals.Numeric.Decimal('19'), 123 | Token.Punctuation.Comma, 124 | Token.Identifiers.FieldName('y'), 125 | Token.Operators.Assignment, 126 | Token.Literals.Numeric.Decimal('23'), 127 | Token.Punctuation.Comma, 128 | Token.Identifiers.FieldName('z'), 129 | Token.Operators.Assignment, 130 | Token.Literals.Numeric.Decimal('42'), 131 | Token.Punctuation.Semicolon, 132 | ]); 133 | }); 134 | 135 | it('type with no names and no modifiers', async () => { 136 | const input = Input.InClass(`public static Integer x;`); 137 | const tokens = await tokenize(input); 138 | 139 | tokens.should.deep.equal([ 140 | Token.Keywords.Modifiers.Public, 141 | Token.Keywords.Modifiers.Static, 142 | Token.PrimitiveType.Integer, 143 | Token.Identifiers.FieldName('x'), 144 | Token.Punctuation.Semicolon, 145 | ]); 146 | }); 147 | 148 | it('Fields with fully-qualified names are highlighted properly (issue omnisharp-vscode#1097)', async () => { 149 | const input = Input.InClass(` 150 | private CanvasGroup[] groups; 151 | private UnityEngine.UI.Image[] selectedImages; 152 | `); 153 | const tokens = await tokenize(input); 154 | 155 | tokens.should.deep.equal([ 156 | Token.Keywords.Modifiers.Private, 157 | Token.Type('CanvasGroup'), 158 | Token.Punctuation.OpenBracket, 159 | Token.Punctuation.CloseBracket, 160 | Token.Identifiers.FieldName('groups'), 161 | Token.Punctuation.Semicolon, 162 | Token.Keywords.Modifiers.Private, 163 | Token.Type('UnityEngine'), 164 | Token.Punctuation.Accessor, 165 | Token.Type('UI'), 166 | Token.Punctuation.Accessor, 167 | Token.Type('Image'), 168 | Token.Punctuation.OpenBracket, 169 | Token.Punctuation.CloseBracket, 170 | Token.Identifiers.FieldName('selectedImages'), 171 | Token.Punctuation.Semicolon, 172 | ]); 173 | }); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/annotation.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2018 Salesforce. All rights reserved. 3 | * See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { should } from 'chai'; 7 | import { tokenize, Input, Token } from './utils/tokenize'; 8 | 9 | describe('Grammar', () => { 10 | before(() => { 11 | should(); 12 | }); 13 | 14 | describe('Annotation', () => { 15 | it('annotation on methods', async () => { 16 | const input = Input.InClass(`@deprecated 17 | // This method is deprecated. Use myOptimizedMethod(String a, String b) instead. 18 | global void myMethod(String a) { 19 | }`); 20 | const tokens = await tokenize(input); 21 | 22 | tokens.should.deep.equal([ 23 | Token.Keywords.Modifiers.AnnotationName('@deprecated'), 24 | Token.Comment.LeadingWhitespace(' '), 25 | Token.Comment.SingleLine.Start, 26 | Token.Comment.SingleLine.Text( 27 | ' This method is deprecated. Use myOptimizedMethod(String a, String b) instead.' 28 | ), 29 | Token.Keywords.Modifiers.Global, 30 | Token.PrimitiveType.Void, 31 | Token.Identifiers.MethodName('myMethod'), 32 | Token.Punctuation.OpenParen, 33 | Token.PrimitiveType.String, 34 | Token.Identifiers.ParameterName('a'), 35 | Token.Punctuation.CloseParen, 36 | Token.Punctuation.OpenBrace, 37 | Token.Punctuation.CloseBrace, 38 | ]); 39 | }); 40 | 41 | it('annotation with one parameter', async () => { 42 | const input = Input.InClass(`@future (callout=true) 43 | public static void doCalloutFromFuture() { 44 | }`); 45 | const tokens = await tokenize(input); 46 | 47 | tokens.should.deep.equal([ 48 | Token.Keywords.Modifiers.AnnotationName('@future'), 49 | Token.Punctuation.OpenParen, 50 | Token.Variables.ReadWrite('callout'), 51 | Token.Operators.Assignment, 52 | Token.Literals.Boolean.True, 53 | Token.Punctuation.CloseParen, 54 | Token.Keywords.Modifiers.Public, 55 | Token.Keywords.Modifiers.Static, 56 | Token.PrimitiveType.Void, 57 | Token.Identifiers.MethodName('doCalloutFromFuture'), 58 | Token.Punctuation.OpenParen, 59 | Token.Punctuation.CloseParen, 60 | Token.Punctuation.OpenBrace, 61 | Token.Punctuation.CloseBrace, 62 | ]); 63 | }); 64 | 65 | it('annotation on class', async () => { 66 | const input = Input.FromText(`@isTest 67 | public class MyTestClass { }`); 68 | 69 | const tokens = await tokenize(input); 70 | 71 | tokens.should.deep.equal([ 72 | Token.Keywords.Modifiers.AnnotationName('@isTest'), 73 | Token.Keywords.Modifiers.Public, 74 | Token.Keywords.Class, 75 | Token.Identifiers.ClassName('MyTestClass'), 76 | Token.Punctuation.OpenBrace, 77 | Token.Punctuation.CloseBrace, 78 | ]); 79 | }); 80 | 81 | it('annotation with multiple parameters', async () => { 82 | const input = Input.FromText( 83 | `@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.')` 84 | ); 85 | const tokens = await tokenize(input); 86 | 87 | tokens.should.deep.equal([ 88 | Token.Keywords.Modifiers.AnnotationName('@InvocableMethod'), 89 | Token.Punctuation.OpenParen, 90 | Token.Variables.ReadWrite('label'), 91 | Token.Operators.Assignment, 92 | Token.Punctuation.String.Begin, 93 | Token.XmlDocComments.String.SingleQuoted.Text('Insert Accounts'), 94 | Token.Punctuation.String.End, 95 | Token.Variables.ReadWrite('description'), 96 | Token.Operators.Assignment, 97 | Token.Punctuation.String.Begin, 98 | Token.XmlDocComments.String.SingleQuoted.Text('Inserts new accounts.'), 99 | Token.Punctuation.String.End, 100 | Token.Punctuation.CloseParen, 101 | ]); 102 | }); 103 | 104 | it('annotation with multiple parameters on field', async () => { 105 | const input = 106 | Input.InClass(`@InvocableMethod(label='Insert Accounts' description='Inserts new accounts.' required=false) 107 | global Id leadId; 108 | `); 109 | const tokens = await tokenize(input); 110 | 111 | tokens.should.deep.equal([ 112 | Token.Keywords.Modifiers.AnnotationName('@InvocableMethod'), 113 | Token.Punctuation.OpenParen, 114 | Token.Variables.ReadWrite('label'), 115 | Token.Operators.Assignment, 116 | Token.Punctuation.String.Begin, 117 | Token.XmlDocComments.String.SingleQuoted.Text('Insert Accounts'), 118 | Token.Punctuation.String.End, 119 | Token.Variables.ReadWrite('description'), 120 | Token.Operators.Assignment, 121 | Token.Punctuation.String.Begin, 122 | Token.XmlDocComments.String.SingleQuoted.Text('Inserts new accounts.'), 123 | Token.Punctuation.String.End, 124 | Token.Variables.ReadWrite('required'), 125 | Token.Operators.Assignment, 126 | Token.Literals.Boolean.False, 127 | Token.Punctuation.CloseParen, 128 | Token.Keywords.Modifiers.Global, 129 | Token.PrimitiveType.ID, 130 | Token.Identifiers.FieldName('leadId'), 131 | Token.Punctuation.Semicolon, 132 | ]); 133 | }); 134 | 135 | it('annotation on same line as method declaration (issue #44)', async () => { 136 | const input = Input.InClass( 137 | `@Future(callout=true) public static void method() {}` 138 | ); 139 | const tokens = await tokenize(input); 140 | 141 | tokens.should.deep.equal([ 142 | Token.Keywords.Modifiers.AnnotationName('@Future'), 143 | Token.Punctuation.OpenParen, 144 | Token.Variables.ReadWrite('callout'), 145 | Token.Operators.Assignment, 146 | Token.Literals.Boolean.True, 147 | Token.Punctuation.CloseParen, 148 | Token.Keywords.Modifiers.Public, 149 | Token.Keywords.Modifiers.Static, 150 | Token.PrimitiveType.Void, 151 | Token.Identifiers.MethodName('method'), 152 | Token.Punctuation.OpenParen, 153 | Token.Punctuation.CloseParen, 154 | Token.Punctuation.OpenBrace, 155 | Token.Punctuation.CloseBrace, 156 | ]); 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/literals.test.ts: -------------------------------------------------------------------------------- 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('Literals', () => { 16 | describe('Booleans', () => { 17 | it('true', async () => { 18 | const input = Input.InClass(`Boolean x = true;`); 19 | const tokens = await tokenize(input); 20 | 21 | tokens.should.deep.equal([ 22 | Token.PrimitiveType.Boolean, 23 | Token.Identifiers.FieldName('x'), 24 | Token.Operators.Assignment, 25 | Token.Literals.Boolean.True, 26 | Token.Punctuation.Semicolon, 27 | ]); 28 | }); 29 | 30 | it('false', async () => { 31 | const input = Input.InClass(`Boolean x = false;`); 32 | const tokens = await tokenize(input); 33 | 34 | tokens.should.deep.equal([ 35 | Token.PrimitiveType.Boolean, 36 | Token.Identifiers.FieldName('x'), 37 | Token.Operators.Assignment, 38 | Token.Literals.Boolean.False, 39 | Token.Punctuation.Semicolon, 40 | ]); 41 | }); 42 | }); 43 | 44 | describe('Chars', () => { 45 | it('empty', async () => { 46 | const input = Input.InMethod(`String x = '';`); 47 | const tokens = await tokenize(input); 48 | 49 | tokens.should.deep.equal([ 50 | Token.PrimitiveType.String, 51 | Token.Identifiers.LocalName('x'), 52 | Token.Operators.Assignment, 53 | Token.Punctuation.String.Begin, 54 | Token.Punctuation.String.End, 55 | Token.Punctuation.Semicolon, 56 | ]); 57 | }); 58 | 59 | it('letter', async () => { 60 | const input = Input.InMethod(`String x = 'a';`); 61 | const tokens = await tokenize(input); 62 | 63 | tokens.should.deep.equal([ 64 | Token.PrimitiveType.String, 65 | Token.Identifiers.LocalName('x'), 66 | Token.Operators.Assignment, 67 | Token.Punctuation.String.Begin, 68 | Token.Literals.String('a'), 69 | Token.Punctuation.String.End, 70 | Token.Punctuation.Semicolon, 71 | ]); 72 | }); 73 | 74 | it('escaped single quote', async () => { 75 | const input = Input.InMethod(`String x = '\\'';`); 76 | const tokens = await tokenize(input); 77 | 78 | tokens.should.deep.equal([ 79 | Token.PrimitiveType.String, 80 | Token.Identifiers.LocalName('x'), 81 | Token.Operators.Assignment, 82 | Token.Punctuation.String.Begin, 83 | Token.Literals.CharacterEscape("\\'"), 84 | Token.Punctuation.String.End, 85 | Token.Punctuation.Semicolon, 86 | ]); 87 | }); 88 | }); 89 | 90 | describe('Numbers', () => { 91 | it('decimal zero', async () => { 92 | const input = Input.InClass(`Integer x = 0;`); 93 | const tokens = await tokenize(input); 94 | 95 | tokens.should.deep.equal([ 96 | Token.PrimitiveType.Integer, 97 | Token.Identifiers.FieldName('x'), 98 | Token.Operators.Assignment, 99 | Token.Literals.Numeric.Decimal('0'), 100 | Token.Punctuation.Semicolon, 101 | ]); 102 | }); 103 | 104 | it('hexadecimal zero', async () => { 105 | const input = Input.InClass(`Integer x = 0x0;`); 106 | const tokens = await tokenize(input); 107 | 108 | tokens.should.deep.equal([ 109 | Token.PrimitiveType.Integer, 110 | Token.Identifiers.FieldName('x'), 111 | Token.Operators.Assignment, 112 | Token.Literals.Numeric.Hexadecimal('0x0'), 113 | Token.Punctuation.Semicolon, 114 | ]); 115 | }); 116 | 117 | it('binary zero', async () => { 118 | const input = Input.InClass(`Integer x = 0b0;`); 119 | const tokens = await tokenize(input); 120 | 121 | tokens.should.deep.equal([ 122 | Token.PrimitiveType.Integer, 123 | Token.Identifiers.FieldName('x'), 124 | Token.Operators.Assignment, 125 | Token.Literals.Numeric.Binary('0b0'), 126 | Token.Punctuation.Semicolon, 127 | ]); 128 | }); 129 | 130 | it('Double zero', async () => { 131 | const input = Input.InClass(`Double x = 0.0;`); 132 | const tokens = await tokenize(input); 133 | 134 | tokens.should.deep.equal([ 135 | Token.PrimitiveType.Double, 136 | Token.Identifiers.FieldName('x'), 137 | Token.Operators.Assignment, 138 | Token.Literals.Numeric.Decimal('0.0'), 139 | Token.Punctuation.Semicolon, 140 | ]); 141 | }); 142 | }); 143 | 144 | describe('Strings', () => { 145 | it('simple', async () => { 146 | const input = Input.InClass(`String test = 'hello world!';`); 147 | const tokens = await tokenize(input); 148 | 149 | tokens.should.deep.equal([ 150 | Token.PrimitiveType.String, 151 | Token.Identifiers.FieldName('test'), 152 | Token.Operators.Assignment, 153 | Token.Punctuation.String.Begin, 154 | Token.Literals.String('hello world!'), 155 | Token.Punctuation.String.End, 156 | Token.Punctuation.Semicolon, 157 | ]); 158 | }); 159 | 160 | it('escaped double-quote', async () => { 161 | const input = Input.InClass(`String test = 'hello \\"world!\\"';`); 162 | const tokens = await tokenize(input); 163 | 164 | tokens.should.deep.equal([ 165 | Token.PrimitiveType.String, 166 | Token.Identifiers.FieldName('test'), 167 | Token.Operators.Assignment, 168 | Token.Punctuation.String.Begin, 169 | Token.Literals.String('hello '), 170 | Token.Literals.CharacterEscape('\\"'), 171 | Token.Literals.String('world!'), 172 | Token.Literals.CharacterEscape('\\"'), 173 | Token.Punctuation.String.End, 174 | Token.Punctuation.Semicolon, 175 | ]); 176 | }); 177 | }); 178 | }); 179 | }); 180 | -------------------------------------------------------------------------------- /test/enum.test.ts: -------------------------------------------------------------------------------- 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('Enums', () => { 16 | it('simple enum', async () => { 17 | const input = `enum E { }`; 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.Keywords.Enum, 22 | Token.Identifiers.EnumName('E'), 23 | Token.Punctuation.OpenBrace, 24 | Token.Punctuation.CloseBrace, 25 | ]); 26 | }); 27 | 28 | it('simple public enum', async () => { 29 | const input = `public enum Season {WINTER, SPRING, SUMMER, FALL}`; 30 | const tokens = await tokenize(input); 31 | 32 | tokens.should.deep.equal([ 33 | Token.Keywords.Modifiers.Public, 34 | Token.Keywords.Enum, 35 | Token.Identifiers.EnumName('Season'), 36 | Token.Punctuation.OpenBrace, 37 | Token.Identifiers.EnumMemberName('WINTER'), 38 | Token.Punctuation.Comma, 39 | Token.Identifiers.EnumMemberName('SPRING'), 40 | Token.Punctuation.Comma, 41 | Token.Identifiers.EnumMemberName('SUMMER'), 42 | Token.Punctuation.Comma, 43 | Token.Identifiers.EnumMemberName('FALL'), 44 | Token.Punctuation.CloseBrace, 45 | ]); 46 | }); 47 | 48 | it('internal public enum', async () => { 49 | const input = Input.InClass( 50 | `public enum Season {WINTER, SPRING, SUMMER, FALL}` 51 | ); 52 | const tokens = await tokenize(input); 53 | 54 | tokens.should.deep.equal([ 55 | Token.Keywords.Modifiers.Public, 56 | Token.Keywords.Enum, 57 | Token.Identifiers.EnumName('Season'), 58 | Token.Punctuation.OpenBrace, 59 | Token.Identifiers.EnumMemberName('WINTER'), 60 | Token.Punctuation.Comma, 61 | Token.Identifiers.EnumMemberName('SPRING'), 62 | Token.Punctuation.Comma, 63 | Token.Identifiers.EnumMemberName('SUMMER'), 64 | Token.Punctuation.Comma, 65 | Token.Identifiers.EnumMemberName('FALL'), 66 | Token.Punctuation.CloseBrace, 67 | ]); 68 | }); 69 | 70 | it('enum with single member', async () => { 71 | const input = `enum E { M1 }`; 72 | const tokens = await tokenize(input); 73 | 74 | tokens.should.deep.equal([ 75 | Token.Keywords.Enum, 76 | Token.Identifiers.EnumName('E'), 77 | Token.Punctuation.OpenBrace, 78 | Token.Identifiers.EnumMemberName('M1'), 79 | Token.Punctuation.CloseBrace, 80 | ]); 81 | }); 82 | 83 | it('enum with multiple members', async () => { 84 | const input = `enum Color { Red, Green, Blue }`; 85 | const tokens = await tokenize(input); 86 | 87 | tokens.should.deep.equal([ 88 | Token.Keywords.Enum, 89 | Token.Identifiers.EnumName('Color'), 90 | Token.Punctuation.OpenBrace, 91 | Token.Identifiers.EnumMemberName('Red'), 92 | Token.Punctuation.Comma, 93 | Token.Identifiers.EnumMemberName('Green'), 94 | Token.Punctuation.Comma, 95 | Token.Identifiers.EnumMemberName('Blue'), 96 | Token.Punctuation.CloseBrace, 97 | ]); 98 | }); 99 | 100 | it('enum with initialized member', async () => { 101 | const input = ` 102 | enum E 103 | { 104 | Value1 = 1, 105 | Value2, 106 | Value3 107 | } 108 | `; 109 | 110 | const tokens = await tokenize(input); 111 | 112 | tokens.should.deep.equal([ 113 | Token.Keywords.Enum, 114 | Token.Identifiers.EnumName('E'), 115 | Token.Punctuation.OpenBrace, 116 | Token.Identifiers.EnumMemberName('Value1'), 117 | Token.Operators.Assignment, 118 | Token.Literals.Numeric.Decimal('1'), 119 | Token.Punctuation.Comma, 120 | Token.Identifiers.EnumMemberName('Value2'), 121 | Token.Punctuation.Comma, 122 | Token.Identifiers.EnumMemberName('Value3'), 123 | Token.Punctuation.CloseBrace, 124 | ]); 125 | }); 126 | 127 | it('enum members are highligted properly (issue omnisharp-vscode#1108)', async () => { 128 | const input = ` 129 | public enum TestEnum 130 | { 131 | enum1, 132 | enum2, 133 | enum3, 134 | enum4 135 | } 136 | 137 | public class TestClass 138 | { 139 | 140 | } 141 | 142 | public enum TestEnum2 143 | { 144 | enum1 = 10, 145 | enum2 = 15, 146 | } 147 | 148 | public class TestClass2 149 | { 150 | 151 | } 152 | `; 153 | 154 | const tokens = await tokenize(input); 155 | 156 | tokens.should.deep.equal([ 157 | Token.Keywords.Modifiers.Public, 158 | Token.Keywords.Enum, 159 | Token.Identifiers.EnumName('TestEnum'), 160 | Token.Punctuation.OpenBrace, 161 | Token.Identifiers.EnumMemberName('enum1'), 162 | Token.Punctuation.Comma, 163 | Token.Identifiers.EnumMemberName('enum2'), 164 | Token.Punctuation.Comma, 165 | Token.Identifiers.EnumMemberName('enum3'), 166 | Token.Punctuation.Comma, 167 | Token.Identifiers.EnumMemberName('enum4'), 168 | Token.Punctuation.CloseBrace, 169 | 170 | Token.Keywords.Modifiers.Public, 171 | Token.Keywords.Class, 172 | Token.Identifiers.ClassName('TestClass'), 173 | Token.Punctuation.OpenBrace, 174 | Token.Punctuation.CloseBrace, 175 | 176 | Token.Keywords.Modifiers.Public, 177 | Token.Keywords.Enum, 178 | Token.Identifiers.EnumName('TestEnum2'), 179 | Token.Punctuation.OpenBrace, 180 | Token.Identifiers.EnumMemberName('enum1'), 181 | Token.Operators.Assignment, 182 | Token.Literals.Numeric.Decimal('10'), 183 | Token.Punctuation.Comma, 184 | Token.Identifiers.EnumMemberName('enum2'), 185 | Token.Operators.Assignment, 186 | Token.Literals.Numeric.Decimal('15'), 187 | Token.Punctuation.Comma, 188 | Token.Punctuation.CloseBrace, 189 | 190 | Token.Keywords.Modifiers.Public, 191 | Token.Keywords.Class, 192 | Token.Identifiers.ClassName('TestClass2'), 193 | Token.Punctuation.OpenBrace, 194 | Token.Punctuation.CloseBrace, 195 | ]); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/constructor.test.ts: -------------------------------------------------------------------------------- 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('Constructors', () => { 16 | it('instance constructor with no parameters', async () => { 17 | const input = Input.InClass(`TestClass() { }`); 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.Identifiers.MethodName('TestClass'), 22 | Token.Punctuation.OpenParen, 23 | Token.Punctuation.CloseParen, 24 | Token.Punctuation.OpenBrace, 25 | Token.Punctuation.CloseBrace, 26 | ]); 27 | }); 28 | 29 | it('public instance constructor with no parameters', async () => { 30 | const input = Input.InClass(`public TestClass() { }`); 31 | const tokens = await tokenize(input); 32 | 33 | tokens.should.deep.equal([ 34 | Token.Keywords.Modifiers.Public, 35 | Token.Identifiers.MethodName('TestClass'), 36 | Token.Punctuation.OpenParen, 37 | Token.Punctuation.CloseParen, 38 | Token.Punctuation.OpenBrace, 39 | Token.Punctuation.CloseBrace, 40 | ]); 41 | }); 42 | 43 | it('public instance constructor with one parameter', async () => { 44 | const input = Input.InClass(`public TestClass(Integer x) { }`); 45 | const tokens = await tokenize(input); 46 | 47 | tokens.should.deep.equal([ 48 | Token.Keywords.Modifiers.Public, 49 | Token.Identifiers.MethodName('TestClass'), 50 | Token.Punctuation.OpenParen, 51 | Token.PrimitiveType.Integer, 52 | Token.Identifiers.ParameterName('x'), 53 | Token.Punctuation.CloseParen, 54 | Token.Punctuation.OpenBrace, 55 | Token.Punctuation.CloseBrace, 56 | ]); 57 | }); 58 | 59 | it('public instance constructor with one ref parameter', async () => { 60 | const input = Input.InClass(`public TestClass(Object x) { }`); 61 | const tokens = await tokenize(input); 62 | 63 | tokens.should.deep.equal([ 64 | Token.Keywords.Modifiers.Public, 65 | Token.Identifiers.MethodName('TestClass'), 66 | Token.Punctuation.OpenParen, 67 | Token.PrimitiveType.Object, 68 | Token.Identifiers.ParameterName('x'), 69 | Token.Punctuation.CloseParen, 70 | Token.Punctuation.OpenBrace, 71 | Token.Punctuation.CloseBrace, 72 | ]); 73 | }); 74 | 75 | it('instance constructor with two parameters', async () => { 76 | const input = Input.InClass(` 77 | TestClass(String x, Integer y) 78 | { 79 | }`); 80 | const tokens = await tokenize(input); 81 | 82 | tokens.should.deep.equal([ 83 | Token.Identifiers.MethodName('TestClass'), 84 | Token.Punctuation.OpenParen, 85 | Token.PrimitiveType.String, 86 | Token.Identifiers.ParameterName('x'), 87 | Token.Punctuation.Comma, 88 | Token.PrimitiveType.Integer, 89 | Token.Identifiers.ParameterName('y'), 90 | Token.Punctuation.CloseParen, 91 | Token.Punctuation.OpenBrace, 92 | Token.Punctuation.CloseBrace, 93 | ]); 94 | }); 95 | 96 | it('static constructor no parameters', async () => { 97 | const input = Input.InClass(`TestClass() { }`); 98 | const tokens = await tokenize(input); 99 | 100 | tokens.should.deep.equal([ 101 | Token.Identifiers.MethodName('TestClass'), 102 | Token.Punctuation.OpenParen, 103 | Token.Punctuation.CloseParen, 104 | Token.Punctuation.OpenBrace, 105 | Token.Punctuation.CloseBrace, 106 | ]); 107 | }); 108 | 109 | it('Open multiline comment in front of parameter highlights properly (issue omnisharp-vscode#861)', async () => { 110 | const input = Input.InClass(` 111 | WaitHandle(Task self) 112 | { 113 | this.task = self; 114 | } 115 | `); 116 | const tokens = await tokenize(input); 117 | 118 | tokens.should.deep.equal([ 119 | Token.Identifiers.MethodName('WaitHandle'), 120 | Token.Punctuation.OpenParen, 121 | Token.Type('Task'), 122 | Token.Identifiers.ParameterName('self'), 123 | Token.Punctuation.CloseParen, 124 | Token.Punctuation.OpenBrace, 125 | Token.Keywords.This, 126 | Token.Punctuation.Accessor, 127 | Token.Variables.Property('task'), 128 | Token.Operators.Assignment, 129 | Token.Variables.ReadWrite('self'), 130 | Token.Punctuation.Semicolon, 131 | Token.Punctuation.CloseBrace, 132 | ]); 133 | }); 134 | 135 | it('closing parenthesis of parameter list on next line', async () => { 136 | const input = Input.InClass(` 137 | public C( 138 | String s 139 | ) 140 | { 141 | }`); 142 | const tokens = await tokenize(input); 143 | 144 | tokens.should.deep.equal([ 145 | Token.Keywords.Modifiers.Public, 146 | Token.Identifiers.MethodName('C'), 147 | Token.Punctuation.OpenParen, 148 | 149 | Token.PrimitiveType.String, 150 | Token.Identifiers.ParameterName('s'), 151 | 152 | Token.Punctuation.CloseParen, 153 | Token.Punctuation.OpenBrace, 154 | Token.Punctuation.CloseBrace, 155 | ]); 156 | }); 157 | 158 | it('closing parenthesis of parameter list on next line (issue #88)', async () => { 159 | const input = Input.InClass(` 160 | public AccountController( 161 | UserManager userManager, 162 | SignInManager signInManager, 163 | ILogger logger 164 | ) 165 | { 166 | }`); 167 | const tokens = await tokenize(input); 168 | 169 | tokens.should.deep.equal([ 170 | Token.Keywords.Modifiers.Public, 171 | Token.Identifiers.MethodName('AccountController'), 172 | Token.Punctuation.OpenParen, 173 | 174 | Token.Type('UserManager'), 175 | Token.Punctuation.TypeParameters.Begin, 176 | Token.Type('User'), 177 | Token.Punctuation.TypeParameters.End, 178 | Token.Identifiers.ParameterName('userManager'), 179 | Token.Punctuation.Comma, 180 | 181 | Token.Type('SignInManager'), 182 | Token.Punctuation.TypeParameters.Begin, 183 | Token.Type('User'), 184 | Token.Punctuation.TypeParameters.End, 185 | Token.Identifiers.ParameterName('signInManager'), 186 | Token.Punctuation.Comma, 187 | 188 | Token.Type('ILogger'), 189 | Token.Punctuation.TypeParameters.Begin, 190 | Token.Type('AccountController'), 191 | Token.Punctuation.TypeParameters.End, 192 | Token.Identifiers.ParameterName('logger'), 193 | 194 | Token.Punctuation.CloseParen, 195 | Token.Punctuation.OpenBrace, 196 | Token.Punctuation.CloseBrace, 197 | ]); 198 | }); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/initializer-block.test.ts: -------------------------------------------------------------------------------- 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 | }); 202 | -------------------------------------------------------------------------------- /test/xml-doc-comment.test.ts: -------------------------------------------------------------------------------- 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('XML Doc Comments', () => { 16 | it('start tag', async () => { 17 | const input = `///

`; 18 | const tokens = await tokenize(input); 19 | 20 | tokens.should.deep.equal([ 21 | Token.XmlDocComments.Begin, 22 | Token.XmlDocComments.Text(' '), 23 | Token.XmlDocComments.Tag.StartTagBegin, 24 | Token.XmlDocComments.Tag.Name('summary'), 25 | Token.XmlDocComments.Tag.StartTagEnd, 26 | ]); 27 | }); 28 | 29 | it('end tag', async () => { 30 | const input = `/// `; 31 | const tokens = await tokenize(input); 32 | 33 | tokens.should.deep.equal([ 34 | Token.XmlDocComments.Begin, 35 | Token.XmlDocComments.Text(' '), 36 | Token.XmlDocComments.Tag.EndTagBegin, 37 | Token.XmlDocComments.Tag.Name('summary'), 38 | Token.XmlDocComments.Tag.EndTagEnd, 39 | ]); 40 | }); 41 | 42 | it('empty tag', async () => { 43 | const input = `/// `; 44 | const tokens = await tokenize(input); 45 | 46 | tokens.should.deep.equal([ 47 | Token.XmlDocComments.Begin, 48 | Token.XmlDocComments.Text(' '), 49 | Token.XmlDocComments.Tag.EmptyTagBegin, 50 | Token.XmlDocComments.Tag.Name('summary'), 51 | Token.XmlDocComments.Tag.EmptyTagEnd, 52 | ]); 53 | }); 54 | 55 | it('start tag with attribute and single-quoted string', async () => { 56 | const input = `/// `; 57 | const tokens = await tokenize(input); 58 | 59 | tokens.should.deep.equal([ 60 | Token.XmlDocComments.Begin, 61 | Token.XmlDocComments.Text(' '), 62 | Token.XmlDocComments.Tag.StartTagBegin, 63 | Token.XmlDocComments.Tag.Name('param'), 64 | Token.XmlDocComments.Attribute.Name('name'), 65 | Token.XmlDocComments.Equals, 66 | Token.XmlDocComments.String.SingleQuoted.Begin, 67 | Token.XmlDocComments.String.SingleQuoted.Text('x'), 68 | Token.XmlDocComments.String.SingleQuoted.End, 69 | Token.XmlDocComments.Tag.StartTagEnd, 70 | ]); 71 | }); 72 | 73 | it('start tag with attribute and double-quoted string', async () => { 74 | const input = `/// `; 75 | const tokens = await tokenize(input); 76 | 77 | tokens.should.deep.equal([ 78 | Token.XmlDocComments.Begin, 79 | Token.XmlDocComments.Text(' '), 80 | Token.XmlDocComments.Tag.StartTagBegin, 81 | Token.XmlDocComments.Tag.Name('param'), 82 | Token.XmlDocComments.Attribute.Name('name'), 83 | Token.XmlDocComments.Equals, 84 | Token.XmlDocComments.String.DoubleQuoted.Begin, 85 | Token.XmlDocComments.String.DoubleQuoted.Text('x'), 86 | Token.XmlDocComments.String.DoubleQuoted.End, 87 | Token.XmlDocComments.Tag.StartTagEnd, 88 | ]); 89 | }); 90 | 91 | it('comment', async () => { 92 | const input = `/// `; 93 | const tokens = await tokenize(input); 94 | 95 | tokens.should.deep.equal([ 96 | Token.XmlDocComments.Begin, 97 | Token.XmlDocComments.Text(' '), 98 | Token.XmlDocComments.Comment.Begin, 99 | Token.XmlDocComments.Comment.Text(' comment '), 100 | Token.XmlDocComments.Comment.End, 101 | ]); 102 | }); 103 | 104 | it('cdata', async () => { 105 | const input = `/// `; 106 | const tokens = await tokenize(input); 107 | 108 | tokens.should.deep.equal([ 109 | Token.XmlDocComments.Begin, 110 | Token.XmlDocComments.Text(' '), 111 | Token.XmlDocComments.CData.Begin, 112 | Token.XmlDocComments.CData.Text('c'), 113 | Token.XmlDocComments.CData.End, 114 | ]); 115 | }); 116 | 117 | it('character entity - name', async () => { 118 | const input = `/// &`; 119 | const tokens = await tokenize(input); 120 | 121 | tokens.should.deep.equal([ 122 | Token.XmlDocComments.Begin, 123 | Token.XmlDocComments.Text(' '), 124 | Token.XmlDocComments.CharacterEntity.Begin, 125 | Token.XmlDocComments.CharacterEntity.Text('amp'), 126 | Token.XmlDocComments.CharacterEntity.End, 127 | ]); 128 | }); 129 | 130 | it('character entity - decimal', async () => { 131 | const input = `/// &`; 132 | const tokens = await tokenize(input); 133 | 134 | tokens.should.deep.equal([ 135 | Token.XmlDocComments.Begin, 136 | Token.XmlDocComments.Text(' '), 137 | Token.XmlDocComments.CharacterEntity.Begin, 138 | Token.XmlDocComments.CharacterEntity.Text('#0038'), 139 | Token.XmlDocComments.CharacterEntity.End, 140 | ]); 141 | }); 142 | 143 | it('character entity - hdex', async () => { 144 | const input = `/// &`; 145 | const tokens = await tokenize(input); 146 | 147 | tokens.should.deep.equal([ 148 | Token.XmlDocComments.Begin, 149 | Token.XmlDocComments.Text(' '), 150 | Token.XmlDocComments.CharacterEntity.Begin, 151 | Token.XmlDocComments.CharacterEntity.Text('#x0026'), 152 | Token.XmlDocComments.CharacterEntity.End, 153 | ]); 154 | }); 155 | 156 | it('XML doc comments are highlighted properly on enum members (issue omnisharp-vscode#706)', async () => { 157 | const input = ` 158 | /// This is a test Enum 159 | public enum TestEnum 160 | { 161 | /// Test Value One 162 | TestValueOne= 0, 163 | /// Test Value Two 164 | TestValueTwo = 1 165 | }`; 166 | 167 | const tokens = await tokenize(input); 168 | 169 | tokens.should.deep.equal([ 170 | Token.XmlDocComments.Begin, 171 | Token.XmlDocComments.Text(' '), 172 | Token.XmlDocComments.Tag.StartTagBegin, 173 | Token.XmlDocComments.Tag.Name('summary'), 174 | Token.XmlDocComments.Tag.StartTagEnd, 175 | Token.XmlDocComments.Text(' This is a test Enum '), 176 | Token.XmlDocComments.Tag.EndTagBegin, 177 | Token.XmlDocComments.Tag.Name('summary'), 178 | Token.XmlDocComments.Tag.EndTagEnd, 179 | Token.Keywords.Modifiers.Public, 180 | Token.Keywords.Enum, 181 | Token.Identifiers.EnumName('TestEnum'), 182 | Token.Punctuation.OpenBrace, 183 | Token.Comment.LeadingWhitespace(' '), 184 | Token.XmlDocComments.Begin, 185 | Token.XmlDocComments.Text(' '), 186 | Token.XmlDocComments.Tag.StartTagBegin, 187 | Token.XmlDocComments.Tag.Name('summary'), 188 | Token.XmlDocComments.Tag.StartTagEnd, 189 | Token.XmlDocComments.Text(' Test Value One '), 190 | Token.XmlDocComments.Tag.EndTagBegin, 191 | Token.XmlDocComments.Tag.Name('summary'), 192 | Token.XmlDocComments.Tag.EndTagEnd, 193 | Token.Identifiers.EnumMemberName('TestValueOne'), 194 | Token.Operators.Assignment, 195 | Token.Literals.Numeric.Decimal('0'), 196 | Token.Punctuation.Comma, 197 | Token.Comment.LeadingWhitespace(' '), 198 | Token.XmlDocComments.Begin, 199 | Token.XmlDocComments.Text(' '), 200 | Token.XmlDocComments.Tag.StartTagBegin, 201 | Token.XmlDocComments.Tag.Name('summary'), 202 | Token.XmlDocComments.Tag.StartTagEnd, 203 | Token.XmlDocComments.Text(' Test Value Two '), 204 | Token.XmlDocComments.Tag.EndTagBegin, 205 | Token.XmlDocComments.Tag.Name('summary'), 206 | Token.XmlDocComments.Tag.EndTagEnd, 207 | Token.Identifiers.EnumMemberName('TestValueTwo'), 208 | Token.Operators.Assignment, 209 | Token.Literals.Numeric.Decimal('1'), 210 | Token.Punctuation.CloseBrace, 211 | ]); 212 | }); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /test/class.test.ts: -------------------------------------------------------------------------------- 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('Apex Class', () => { 16 | it('class keyword and storage modifiers', async () => { 17 | const input = Input.FromText(` 18 | public class PublicClass { } 19 | class DefaultClass { } 20 | protected class ProtectedClass { } 21 | global class DefaultGlobalClass { } 22 | public with sharing class PublicWithSharingClass { } 23 | without sharing class DefaultWithoutSharingClass { } 24 | public virtual class PublicVirtualClass { } 25 | public abstract class PublicAbstractClass { } 26 | abstract class DefaultAbstractClass { }`); 27 | 28 | const tokens = await tokenize(input); 29 | 30 | tokens.should.deep.equal([ 31 | Token.Keywords.Modifiers.Public, 32 | Token.Keywords.Class, 33 | Token.Identifiers.ClassName('PublicClass'), 34 | Token.Punctuation.OpenBrace, 35 | Token.Punctuation.CloseBrace, 36 | 37 | Token.Keywords.Class, 38 | Token.Identifiers.ClassName('DefaultClass'), 39 | Token.Punctuation.OpenBrace, 40 | Token.Punctuation.CloseBrace, 41 | 42 | Token.Keywords.Modifiers.Protected, 43 | Token.Keywords.Class, 44 | Token.Identifiers.ClassName('ProtectedClass'), 45 | Token.Punctuation.OpenBrace, 46 | Token.Punctuation.CloseBrace, 47 | 48 | Token.Keywords.Modifiers.Global, 49 | Token.Keywords.Class, 50 | Token.Identifiers.ClassName('DefaultGlobalClass'), 51 | Token.Punctuation.OpenBrace, 52 | Token.Punctuation.CloseBrace, 53 | 54 | Token.Keywords.Modifiers.Public, 55 | Token.Keywords.Modifiers.WithSharing, 56 | Token.Keywords.Class, 57 | Token.Identifiers.ClassName('PublicWithSharingClass'), 58 | Token.Punctuation.OpenBrace, 59 | Token.Punctuation.CloseBrace, 60 | 61 | Token.Keywords.Modifiers.WithoutSharing, 62 | Token.Keywords.Class, 63 | Token.Identifiers.ClassName('DefaultWithoutSharingClass'), 64 | Token.Punctuation.OpenBrace, 65 | Token.Punctuation.CloseBrace, 66 | 67 | Token.Keywords.Modifiers.Public, 68 | Token.Keywords.Modifiers.Virtual, 69 | Token.Keywords.Class, 70 | Token.Identifiers.ClassName('PublicVirtualClass'), 71 | Token.Punctuation.OpenBrace, 72 | Token.Punctuation.CloseBrace, 73 | 74 | Token.Keywords.Modifiers.Public, 75 | Token.Keywords.Modifiers.Abstract, 76 | Token.Keywords.Class, 77 | Token.Identifiers.ClassName('PublicAbstractClass'), 78 | Token.Punctuation.OpenBrace, 79 | Token.Punctuation.CloseBrace, 80 | 81 | Token.Keywords.Modifiers.Abstract, 82 | Token.Keywords.Class, 83 | Token.Identifiers.ClassName('DefaultAbstractClass'), 84 | Token.Punctuation.OpenBrace, 85 | Token.Punctuation.CloseBrace, 86 | ]); 87 | }); 88 | 89 | it('public class with sharing', async () => { 90 | const input = Input.FromText(`public with sharing class C {}`); 91 | const tokens = await tokenize(input); 92 | 93 | tokens.should.deep.equal([ 94 | Token.Keywords.Modifiers.Public, 95 | Token.Keywords.Modifiers.WithSharing, 96 | Token.Keywords.Class, 97 | Token.Identifiers.ClassName('C'), 98 | Token.Punctuation.OpenBrace, 99 | Token.Punctuation.CloseBrace, 100 | ]); 101 | }); 102 | 103 | it('public class without sharing', async () => { 104 | const input = Input.FromText(`public without sharing class Fireburn {}`); 105 | const tokens = await tokenize(input); 106 | 107 | tokens.should.deep.equal([ 108 | Token.Keywords.Modifiers.Public, 109 | Token.Keywords.Modifiers.WithoutSharing, 110 | Token.Keywords.Class, 111 | Token.Identifiers.ClassName('Fireburn'), 112 | Token.Punctuation.OpenBrace, 113 | Token.Punctuation.CloseBrace, 114 | ]); 115 | }); 116 | 117 | it('simple class', async () => { 118 | const input = Input.FromText(`private class SimpleClass {}`); 119 | const tokens = await tokenize(input); 120 | 121 | tokens.should.deep.equal([ 122 | Token.Keywords.Modifiers.Private, 123 | Token.Keywords.Class, 124 | Token.Identifiers.ClassName('SimpleClass'), 125 | Token.Punctuation.OpenBrace, 126 | Token.Punctuation.CloseBrace, 127 | ]); 128 | }); 129 | 130 | it('global class', async () => { 131 | const input = Input.FromText(`global class GlobalClass {}`); 132 | const tokens = await tokenize(input); 133 | 134 | tokens.should.deep.equal([ 135 | Token.Keywords.Modifiers.Global, 136 | Token.Keywords.Class, 137 | Token.Identifiers.ClassName('GlobalClass'), 138 | Token.Punctuation.OpenBrace, 139 | Token.Punctuation.CloseBrace, 140 | ]); 141 | }); 142 | 143 | it('private class extends', async () => { 144 | const input = Input.FromText(`private class Car extends Vehicle {}`); 145 | const tokens = await tokenize(input); 146 | 147 | tokens.should.deep.equal([ 148 | Token.Keywords.Modifiers.Private, 149 | Token.Keywords.Class, 150 | Token.Identifiers.ClassName('Car'), 151 | Token.Keywords.Extends, 152 | Token.Identifiers.ExtendsName('Vehicle'), 153 | Token.Punctuation.OpenBrace, 154 | Token.Punctuation.CloseBrace, 155 | ]); 156 | }); 157 | 158 | it('class extends implements', async () => { 159 | const input = Input.FromText( 160 | `public abstract class MySecondException extends Exception implements MyInterface {}` 161 | ); 162 | const tokens = await tokenize(input); 163 | 164 | tokens.should.deep.equal([ 165 | Token.Keywords.Modifiers.Public, 166 | Token.Keywords.Modifiers.Abstract, 167 | Token.Keywords.Class, 168 | Token.Identifiers.ClassName('MySecondException'), 169 | Token.Keywords.Extends, 170 | Token.Identifiers.ExtendsName('Exception'), 171 | Token.Keywords.Implements, 172 | Token.Identifiers.ImplementsName('MyInterface'), 173 | Token.Punctuation.OpenBrace, 174 | Token.Punctuation.CloseBrace, 175 | ]); 176 | }); 177 | 178 | it('class implements extends', async () => { 179 | const input = Input.FromText( 180 | `public abstract class MySecondException implements MyInterface extends Exception {}` 181 | ); 182 | const tokens = await tokenize(input); 183 | 184 | tokens.should.deep.equal([ 185 | Token.Keywords.Modifiers.Public, 186 | Token.Keywords.Modifiers.Abstract, 187 | Token.Keywords.Class, 188 | Token.Identifiers.ClassName('MySecondException'), 189 | Token.Keywords.Implements, 190 | Token.Identifiers.ImplementsName('MyInterface'), 191 | Token.Keywords.Extends, 192 | Token.Identifiers.ExtendsName('Exception'), 193 | Token.Punctuation.OpenBrace, 194 | Token.Punctuation.CloseBrace, 195 | ]); 196 | }); 197 | 198 | it('class implements multiple', async () => { 199 | const input = Input.FromText( 200 | `public abstract class MySecondException implements MyInterface, MyInterface2, MyInterface3 {}` 201 | ); 202 | const tokens = await tokenize(input); 203 | 204 | tokens.should.deep.equal([ 205 | Token.Keywords.Modifiers.Public, 206 | Token.Keywords.Modifiers.Abstract, 207 | Token.Keywords.Class, 208 | Token.Identifiers.ClassName('MySecondException'), 209 | Token.Keywords.Implements, 210 | Token.Identifiers.ImplementsName('MyInterface'), 211 | Token.Punctuation.Comma, 212 | Token.Identifiers.ImplementsName('MyInterface2'), 213 | Token.Punctuation.Comma, 214 | Token.Identifiers.ImplementsName('MyInterface3'), 215 | Token.Punctuation.OpenBrace, 216 | Token.Punctuation.CloseBrace, 217 | ]); 218 | }); 219 | 220 | it('class extends namespace-qualified type (issue #50)', async () => { 221 | const input = Input.FromText(`class MyClass extends System.Exception {}`); 222 | const tokens = await tokenize(input); 223 | 224 | tokens.should.deep.equal([ 225 | Token.Keywords.Class, 226 | Token.Identifiers.ClassName('MyClass'), 227 | Token.Keywords.Extends, 228 | Token.Support.Class.System, 229 | Token.Punctuation.Accessor, 230 | Token.Support.Class.TypeText('Exception'), 231 | Token.Punctuation.OpenBrace, 232 | Token.Punctuation.CloseBrace, 233 | ]); 234 | }); 235 | 236 | it('class implements namespace-qualified type (issue #50)', async () => { 237 | const input = Input.FromText( 238 | `class MyClass implements Database.Batchable {}` 239 | ); 240 | const tokens = await tokenize(input); 241 | 242 | tokens.should.deep.equal([ 243 | Token.Keywords.Class, 244 | Token.Identifiers.ClassName('MyClass'), 245 | Token.Keywords.Implements, 246 | Token.Support.Class.Database, 247 | Token.Punctuation.Accessor, 248 | Token.Support.Class.TypeText('Batchable'), 249 | Token.Punctuation.TypeParameters.Begin, 250 | Token.Type('Account'), 251 | Token.Punctuation.TypeParameters.End, 252 | Token.Punctuation.OpenBrace, 253 | Token.Punctuation.CloseBrace, 254 | ]); 255 | }); 256 | }); 257 | }); 258 | -------------------------------------------------------------------------------- /test/for-statements.test.ts: -------------------------------------------------------------------------------- 1 | import { should } from 'chai'; 2 | import { tokenize, Input, Token } from './utils/tokenize'; 3 | 4 | describe('Grammar', () => { 5 | before(() => { 6 | should(); 7 | }); 8 | 9 | describe('For-Statements', () => { 10 | it('single-line for loop', async () => { 11 | const input = Input.InMethod(`for (Integer i = 0; i < 42; i++) { }`); 12 | const tokens = await tokenize(input); 13 | 14 | tokens.should.deep.equal([ 15 | Token.Keywords.Control.For, 16 | Token.Punctuation.OpenParen, 17 | Token.PrimitiveType.Integer, 18 | Token.Identifiers.LocalName('i'), 19 | Token.Operators.Assignment, 20 | Token.Literals.Numeric.Decimal('0'), 21 | Token.Punctuation.Semicolon, 22 | Token.Variables.ReadWrite('i'), 23 | Token.Operators.Relational.LessThan, 24 | Token.Literals.Numeric.Decimal('42'), 25 | Token.Punctuation.Semicolon, 26 | Token.Variables.ReadWrite('i'), 27 | Token.Operators.Increment, 28 | Token.Punctuation.CloseParen, 29 | Token.Punctuation.OpenBrace, 30 | Token.Punctuation.CloseBrace, 31 | ]); 32 | }); 33 | 34 | it('for loop with break', async () => { 35 | const input = Input.InMethod(` 36 | for (Integer i = 0; i < 42; i++) 37 | { 38 | break; 39 | }`); 40 | const tokens = await tokenize(input); 41 | 42 | tokens.should.deep.equal([ 43 | Token.Keywords.Control.For, 44 | Token.Punctuation.OpenParen, 45 | Token.PrimitiveType.Integer, 46 | Token.Identifiers.LocalName('i'), 47 | Token.Operators.Assignment, 48 | Token.Literals.Numeric.Decimal('0'), 49 | Token.Punctuation.Semicolon, 50 | Token.Variables.ReadWrite('i'), 51 | Token.Operators.Relational.LessThan, 52 | Token.Literals.Numeric.Decimal('42'), 53 | Token.Punctuation.Semicolon, 54 | Token.Variables.ReadWrite('i'), 55 | Token.Operators.Increment, 56 | Token.Punctuation.CloseParen, 57 | Token.Punctuation.OpenBrace, 58 | Token.Keywords.Control.Break, 59 | Token.Punctuation.Semicolon, 60 | Token.Punctuation.CloseBrace, 61 | ]); 62 | }); 63 | 64 | it('for loop with continue', async () => { 65 | const input = Input.InMethod(` 66 | for (Integer i = 0; i < 42; i++) 67 | { 68 | continue; 69 | }`); 70 | const tokens = await tokenize(input); 71 | 72 | tokens.should.deep.equal([ 73 | Token.Keywords.Control.For, 74 | Token.Punctuation.OpenParen, 75 | Token.PrimitiveType.Integer, 76 | Token.Identifiers.LocalName('i'), 77 | Token.Operators.Assignment, 78 | Token.Literals.Numeric.Decimal('0'), 79 | Token.Punctuation.Semicolon, 80 | Token.Variables.ReadWrite('i'), 81 | Token.Operators.Relational.LessThan, 82 | Token.Literals.Numeric.Decimal('42'), 83 | Token.Punctuation.Semicolon, 84 | Token.Variables.ReadWrite('i'), 85 | Token.Operators.Increment, 86 | Token.Punctuation.CloseParen, 87 | Token.Punctuation.OpenBrace, 88 | Token.Keywords.Control.Continue, 89 | Token.Punctuation.Semicolon, 90 | Token.Punctuation.CloseBrace, 91 | ]); 92 | }); 93 | 94 | it('for loop on collection', async () => { 95 | const input = Input.InMethod(` 96 | for (Integer i : listOfIntegers) 97 | { 98 | continue; 99 | }`); 100 | const tokens = await tokenize(input); 101 | 102 | tokens.should.deep.equal([ 103 | Token.Keywords.Control.For, 104 | Token.Punctuation.OpenParen, 105 | Token.PrimitiveType.Integer, 106 | Token.Identifiers.LocalName('i'), 107 | Token.Keywords.Control.ColonIterator, 108 | Token.Variables.ReadWrite('listOfIntegers'), 109 | Token.Punctuation.CloseParen, 110 | Token.Punctuation.OpenBrace, 111 | Token.Keywords.Control.Continue, 112 | Token.Punctuation.Semicolon, 113 | Token.Punctuation.CloseBrace, 114 | ]); 115 | }); 116 | 117 | it('for loop with a type, a query and a comment', async () => { 118 | const input = Input.InMethod(` 119 | for (Account a : [SELECT Id, Name FROM Account]){ 120 | // break; 121 | }`); 122 | const tokens = await tokenize(input); 123 | 124 | tokens.should.deep.equal([ 125 | Token.Keywords.Control.For, 126 | Token.Punctuation.OpenParen, 127 | Token.Type('Account'), 128 | Token.Identifiers.LocalName('a'), 129 | Token.Keywords.Control.ColonIterator, 130 | Token.Punctuation.OpenBracket, 131 | Token.Keywords.Queries.Select, 132 | Token.Keywords.Queries.FieldName('Id'), 133 | Token.Punctuation.Comma, 134 | Token.Keywords.Queries.FieldName('Name'), 135 | Token.Keywords.Queries.From, 136 | Token.Keywords.Queries.TypeName('Account'), 137 | Token.Punctuation.CloseBracket, 138 | Token.Punctuation.CloseParen, 139 | Token.Punctuation.OpenBrace, 140 | Token.Comment.LeadingWhitespace(' '), 141 | Token.Comment.SingleLine.Start, 142 | Token.Comment.SingleLine.Text(' break;'), 143 | Token.Punctuation.CloseBrace, 144 | ]); 145 | }); 146 | 147 | it('for loop with support types', async () => { 148 | const input = Input.InMethod(` 149 | for (SObject myFancyObject : [SELECT Id, Name FROM Account]){ 150 | break; 151 | }`); 152 | const tokens = await tokenize(input); 153 | 154 | tokens.should.deep.equal([ 155 | Token.Keywords.Control.For, 156 | Token.Punctuation.OpenParen, 157 | Token.Support.Class.Text('SObject'), 158 | Token.Identifiers.LocalName('myFancyObject'), 159 | Token.Keywords.Control.ColonIterator, 160 | Token.Punctuation.OpenBracket, 161 | Token.Keywords.Queries.Select, 162 | Token.Keywords.Queries.FieldName('Id'), 163 | Token.Punctuation.Comma, 164 | Token.Keywords.Queries.FieldName('Name'), 165 | Token.Keywords.Queries.From, 166 | Token.Keywords.Queries.TypeName('Account'), 167 | Token.Punctuation.CloseBracket, 168 | 169 | Token.Punctuation.CloseParen, 170 | Token.Punctuation.OpenBrace, 171 | Token.Keywords.Control.Break, 172 | Token.Punctuation.Semicolon, 173 | Token.Punctuation.CloseBrace, 174 | ]); 175 | }); 176 | 177 | it('for loop of an array or set', async () => { 178 | const input = Input.InMethod(` 179 | for (SObject myFancyObject : SomeArrayOrMap){ 180 | break; 181 | }`); 182 | const tokens = await tokenize(input); 183 | 184 | tokens.should.deep.equal([ 185 | Token.Keywords.Control.For, 186 | Token.Punctuation.OpenParen, 187 | Token.Support.Class.Text('SObject'), 188 | Token.Identifiers.LocalName('myFancyObject'), 189 | Token.Keywords.Control.ColonIterator, 190 | Token.Variables.ReadWrite('SomeArrayOrMap'), 191 | Token.Punctuation.CloseParen, 192 | Token.Punctuation.OpenBrace, 193 | Token.Keywords.Control.Break, 194 | Token.Punctuation.Semicolon, 195 | Token.Punctuation.CloseBrace, 196 | ]); 197 | }); 198 | 199 | it('for loop or an object', async () => { 200 | const input = Input.InMethod(` 201 | for (SObject myFancyObject : myObject.WithMethod()){ 202 | break; 203 | }`); 204 | const tokens = await tokenize(input); 205 | 206 | tokens.should.deep.equal([ 207 | Token.Keywords.Control.For, 208 | Token.Punctuation.OpenParen, 209 | Token.Support.Class.Text('SObject'), 210 | Token.Identifiers.LocalName('myFancyObject'), 211 | Token.Keywords.Control.ColonIterator, 212 | Token.Variables.Object('myObject'), 213 | Token.Punctuation.Accessor, 214 | Token.Identifiers.MethodName('WithMethod'), 215 | Token.Punctuation.OpenParen, 216 | Token.Punctuation.CloseParen, 217 | Token.Punctuation.CloseParen, 218 | Token.Punctuation.OpenBrace, 219 | Token.Keywords.Control.Break, 220 | Token.Punctuation.Semicolon, 221 | Token.Punctuation.CloseBrace, 222 | ]); 223 | }); 224 | 225 | it('for loop or an object with safe navigator', async () => { 226 | const input = Input.InMethod(` 227 | for (SObject myFancyObject : myObject?.WithMethod()){ 228 | break; 229 | }`); 230 | const tokens = await tokenize(input); 231 | 232 | tokens.should.deep.equal([ 233 | Token.Keywords.Control.For, 234 | Token.Punctuation.OpenParen, 235 | Token.Support.Class.Text('SObject'), 236 | Token.Identifiers.LocalName('myFancyObject'), 237 | Token.Keywords.Control.ColonIterator, 238 | Token.Variables.Object('myObject'), 239 | Token.Operators.SafeNavigation, 240 | Token.Identifiers.MethodName('WithMethod'), 241 | Token.Punctuation.OpenParen, 242 | Token.Punctuation.CloseParen, 243 | Token.Punctuation.CloseParen, 244 | Token.Punctuation.OpenBrace, 245 | Token.Keywords.Control.Break, 246 | Token.Punctuation.Semicolon, 247 | Token.Punctuation.CloseBrace, 248 | ]); 249 | }); 250 | 251 | it('for loop a query that uses local variables', async () => { 252 | const input = Input.InMethod(` 253 | for (SObject myFancyObject : [SELECT Id, Name FROM User WHERE Id IN :variable]){ 254 | System.debug('This is a test' + myFancyObject); 255 | }`); 256 | const tokens = await tokenize(input); 257 | 258 | tokens.should.deep.equal([ 259 | Token.Keywords.Control.For, 260 | Token.Punctuation.OpenParen, 261 | Token.Support.Class.Text('SObject'), 262 | Token.Identifiers.LocalName('myFancyObject'), 263 | Token.Keywords.Control.ColonIterator, 264 | Token.Punctuation.OpenBracket, 265 | Token.Keywords.Queries.Select, 266 | Token.Keywords.Queries.FieldName('Id'), 267 | Token.Punctuation.Comma, 268 | Token.Keywords.Queries.FieldName('Name'), 269 | Token.Keywords.Queries.From, 270 | Token.Keywords.Queries.TypeName('User'), 271 | Token.Keywords.Queries.Where, 272 | Token.Keywords.Queries.FieldName('Id'), 273 | Token.Keywords.Queries.OperatorName('IN'), 274 | Token.Operators.Conditional.Colon, 275 | Token.Identifiers.LocalName('variable'), 276 | Token.Punctuation.CloseBracket, 277 | Token.Punctuation.CloseParen, 278 | Token.Punctuation.OpenBrace, 279 | Token.Support.Class.System, 280 | Token.Punctuation.Accessor, 281 | Token.Support.Class.FunctionText('debug'), 282 | Token.Punctuation.OpenParen, 283 | Token.XmlDocComments.String.SingleQuoted.Begin, 284 | Token.XmlDocComments.String.SingleQuoted.Text('This is a test'), 285 | Token.XmlDocComments.String.SingleQuoted.End, 286 | Token.Operators.Arithmetic.Addition, 287 | Token.Variables.ReadWrite('myFancyObject'), 288 | Token.Punctuation.CloseParen, 289 | Token.Punctuation.Semicolon, 290 | Token.Punctuation.CloseBrace, 291 | ]); 292 | }); 293 | }); 294 | }); 295 | -------------------------------------------------------------------------------- /test/trigger.test.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) 2018 Salesforce. All rights reserved. 3 | * See LICENSE in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import { should } from 'chai'; 7 | import { tokenize, Input, Token } from './utils/tokenize'; 8 | 9 | describe('Grammar', () => { 10 | before(() => { 11 | should(); 12 | }); 13 | 14 | describe('Apex Trigger', () => { 15 | it('before insert before update Account trigger', async () => { 16 | const input = 17 | Input.FromText(`trigger myAccountTrigger on Account (before insert, after update) { 18 | // Your code here 19 | if(true) {} 20 | }`); 21 | const tokens = await tokenize(input); 22 | 23 | tokens.should.deep.equal([ 24 | Token.Keywords.Trigger, 25 | Token.Identifiers.TriggerName('myAccountTrigger'), 26 | Token.Keywords.Triggers.On, 27 | Token.Type('Account'), 28 | Token.Punctuation.OpenParen, 29 | Token.Keywords.Triggers.Before, 30 | Token.Keywords.Triggers.OperatorName('insert'), 31 | Token.Punctuation.Comma, 32 | Token.Keywords.Triggers.After, 33 | Token.Keywords.Triggers.OperatorName('update'), 34 | Token.Punctuation.CloseParen, 35 | Token.Punctuation.OpenBrace, 36 | Token.Comment.LeadingWhitespace(' '), 37 | Token.Comment.SingleLine.Start, 38 | Token.Comment.SingleLine.Text(' Your code here'), 39 | Token.Keywords.Control.If, 40 | Token.Punctuation.OpenParen, 41 | Token.Literals.Boolean.True, 42 | Token.Punctuation.CloseParen, 43 | Token.Punctuation.OpenBrace, 44 | Token.Punctuation.CloseBrace, 45 | Token.Punctuation.CloseBrace, 46 | ]); 47 | }); 48 | 49 | it('context variables specific to triggers used in methods', async () => { 50 | const input = Input.InMethod( 51 | `if (Trigger.isBefore && Trigger.isInsert) {}` 52 | ); 53 | const tokens = await tokenize(input); 54 | 55 | tokens.should.deep.equal([ 56 | Token.Keywords.Control.If, 57 | Token.Punctuation.OpenParen, 58 | Token.Support.Class.Trigger, 59 | Token.Punctuation.Accessor, 60 | Token.Support.Type.TriggerText('isBefore'), 61 | Token.Operators.Logical.And, 62 | Token.Support.Class.Trigger, 63 | Token.Punctuation.Accessor, 64 | Token.Support.Type.TriggerText('isInsert'), 65 | Token.Punctuation.CloseParen, 66 | Token.Punctuation.OpenBrace, 67 | Token.Punctuation.CloseBrace, 68 | ]); 69 | }); 70 | 71 | it('context variables specific to triggers', async () => { 72 | const input = Input.InTrigger( 73 | `if (Trigger.isBefore && Trigger.isInsert) {}` 74 | ); 75 | const tokens = await tokenize(input); 76 | 77 | tokens.should.deep.equal([ 78 | Token.Keywords.Control.If, 79 | Token.Punctuation.OpenParen, 80 | Token.Support.Class.Trigger, 81 | Token.Punctuation.Accessor, 82 | Token.Support.Type.TriggerText('isBefore'), 83 | Token.Operators.Logical.And, 84 | Token.Support.Class.Trigger, 85 | Token.Punctuation.Accessor, 86 | Token.Support.Type.TriggerText('isInsert'), 87 | Token.Punctuation.CloseParen, 88 | Token.Punctuation.OpenBrace, 89 | Token.Punctuation.CloseBrace, 90 | ]); 91 | }); 92 | 93 | it('triggers language specific functions', async () => { 94 | const input = Input.InTrigger(`Trigger.newMap.keySet();`); 95 | const tokens = await tokenize(input); 96 | 97 | tokens.should.deep.equal([ 98 | Token.Support.Class.Trigger, 99 | Token.Punctuation.Accessor, 100 | Token.Support.Type.TriggerText('newMap'), 101 | Token.Punctuation.Accessor, 102 | Token.Support.Function.TriggerText('keySet'), 103 | Token.Punctuation.OpenParen, 104 | Token.Punctuation.CloseParen, 105 | Token.Punctuation.Semicolon, 106 | ]); 107 | }); 108 | 109 | it('triggers language specific functions - complex scenario', async () => { 110 | const input = Input.InTrigger( 111 | `Trigger.newMap.get(q.opportunity__c).addError('Cannot delete quote');` 112 | ); 113 | const tokens = await tokenize(input); 114 | 115 | tokens.should.deep.equal([ 116 | Token.Support.Class.Trigger, 117 | Token.Punctuation.Accessor, 118 | Token.Support.Type.TriggerText('newMap'), 119 | Token.Punctuation.Accessor, 120 | Token.Support.Function.TriggerText('get'), 121 | Token.Punctuation.OpenParen, 122 | Token.Variables.Object('q'), 123 | Token.Punctuation.Accessor, 124 | Token.Variables.Property('opportunity__c'), 125 | Token.Punctuation.CloseParen, 126 | Token.Punctuation.Accessor, 127 | Token.Support.Function.TriggerText('addError'), 128 | Token.Punctuation.OpenParen, 129 | Token.Punctuation.String.Begin, 130 | Token.XmlDocComments.String.SingleQuoted.Text('Cannot delete quote'), 131 | Token.Punctuation.String.End, 132 | Token.Punctuation.CloseParen, 133 | Token.Punctuation.Semicolon, 134 | ]); 135 | }); 136 | 137 | it('triggers language specific functions - complex scenario with safe navigator', async () => { 138 | const input = Input.InTrigger( 139 | `Trigger.newMap.get(q.opportunity__c)?.addError('Cannot delete quote');` 140 | ); 141 | const tokens = await tokenize(input); 142 | 143 | tokens.should.deep.equal([ 144 | Token.Support.Class.Trigger, 145 | Token.Punctuation.Accessor, 146 | Token.Support.Type.TriggerText('newMap'), 147 | Token.Punctuation.Accessor, 148 | Token.Support.Function.TriggerText('get'), 149 | Token.Punctuation.OpenParen, 150 | Token.Variables.Object('q'), 151 | Token.Punctuation.Accessor, 152 | Token.Variables.Property('opportunity__c'), 153 | Token.Punctuation.CloseParen, 154 | Token.Operators.SafeNavigation, 155 | Token.Support.Function.TriggerText('addError'), 156 | Token.Punctuation.OpenParen, 157 | Token.Punctuation.String.Begin, 158 | Token.XmlDocComments.String.SingleQuoted.Text('Cannot delete quote'), 159 | Token.Punctuation.String.End, 160 | Token.Punctuation.CloseParen, 161 | Token.Punctuation.Semicolon, 162 | ]); 163 | }); 164 | 165 | it('SOQL in triggers', async () => { 166 | const input = Input.InTrigger( 167 | `Contact[] cons = [SELECT LastName FROM Contact WHERE AccountId IN :Trigger.new];` 168 | ); 169 | const tokens = await tokenize(input); 170 | 171 | tokens.should.deep.equal([ 172 | Token.Type('Contact'), 173 | Token.Punctuation.OpenBracket, 174 | Token.Punctuation.CloseBracket, 175 | Token.Identifiers.LocalName('cons'), 176 | Token.Operators.Assignment, 177 | Token.Punctuation.OpenBracket, 178 | Token.Keywords.Queries.Select, 179 | Token.Keywords.Queries.FieldName('LastName'), 180 | Token.Keywords.Queries.From, 181 | Token.Keywords.Queries.TypeName('Contact'), 182 | Token.Keywords.Queries.Where, 183 | Token.Keywords.Queries.FieldName('AccountId'), 184 | Token.Keywords.Queries.OperatorName('IN'), 185 | Token.Operators.Conditional.Colon, 186 | Token.Support.Class.Trigger, 187 | Token.Punctuation.Accessor, 188 | Token.Support.Type.TriggerText('new'), 189 | Token.Punctuation.CloseBracket, 190 | Token.Punctuation.Semicolon, 191 | ]); 192 | }); 193 | 194 | it('SOQL in triggers using methods in clauses', async () => { 195 | const input = Input.InTrigger( 196 | `Contact[] cons = [SELECT LastName FROM Contact WHERE AccountId IN :keys('w')];` 197 | ); 198 | const tokens = await tokenize(input); 199 | 200 | tokens.should.deep.equal([ 201 | Token.Type('Contact'), 202 | Token.Punctuation.OpenBracket, 203 | Token.Punctuation.CloseBracket, 204 | Token.Identifiers.LocalName('cons'), 205 | Token.Operators.Assignment, 206 | Token.Punctuation.OpenBracket, 207 | Token.Keywords.Queries.Select, 208 | Token.Keywords.Queries.FieldName('LastName'), 209 | Token.Keywords.Queries.From, 210 | Token.Keywords.Queries.TypeName('Contact'), 211 | Token.Keywords.Queries.Where, 212 | Token.Keywords.Queries.FieldName('AccountId'), 213 | Token.Keywords.Queries.OperatorName('IN'), 214 | Token.Operators.Conditional.Colon, 215 | Token.Identifiers.MethodName('keys'), 216 | Token.Punctuation.OpenParen, 217 | Token.Punctuation.String.Begin, 218 | Token.XmlDocComments.String.SingleQuoted.Text('w'), 219 | Token.Punctuation.String.End, 220 | Token.Punctuation.CloseParen, 221 | Token.Punctuation.CloseBracket, 222 | Token.Punctuation.Semicolon, 223 | ]); 224 | }); 225 | 226 | it('SOQL in triggers using objects in clauses', async () => { 227 | const input = Input.InTrigger( 228 | `Contact[] cons = [SELECT LastName FROM Contact WHERE AccountId IN :myObject.keys('w')];` 229 | ); 230 | const tokens = await tokenize(input); 231 | 232 | tokens.should.deep.equal([ 233 | Token.Type('Contact'), 234 | Token.Punctuation.OpenBracket, 235 | Token.Punctuation.CloseBracket, 236 | Token.Identifiers.LocalName('cons'), 237 | Token.Operators.Assignment, 238 | Token.Punctuation.OpenBracket, 239 | Token.Keywords.Queries.Select, 240 | Token.Keywords.Queries.FieldName('LastName'), 241 | Token.Keywords.Queries.From, 242 | Token.Keywords.Queries.TypeName('Contact'), 243 | Token.Keywords.Queries.Where, 244 | Token.Keywords.Queries.FieldName('AccountId'), 245 | Token.Keywords.Queries.OperatorName('IN'), 246 | Token.Operators.Conditional.Colon, 247 | Token.Variables.Object('myObject'), 248 | Token.Punctuation.Accessor, 249 | Token.Identifiers.MethodName('keys'), 250 | Token.Punctuation.OpenParen, 251 | Token.Punctuation.String.Begin, 252 | Token.XmlDocComments.String.SingleQuoted.Text('w'), 253 | Token.Punctuation.String.End, 254 | Token.Punctuation.CloseParen, 255 | Token.Punctuation.CloseBracket, 256 | Token.Punctuation.Semicolon, 257 | ]); 258 | }); 259 | 260 | it('SOQL in triggers using objects in clauses with safe navigator', async () => { 261 | const input = Input.InTrigger( 262 | `Contact[] cons = [SELECT LastName FROM Contact WHERE AccountId IN :myObject?.keys('w')];` 263 | ); 264 | const tokens = await tokenize(input); 265 | 266 | tokens.should.deep.equal([ 267 | Token.Type('Contact'), 268 | Token.Punctuation.OpenBracket, 269 | Token.Punctuation.CloseBracket, 270 | Token.Identifiers.LocalName('cons'), 271 | Token.Operators.Assignment, 272 | Token.Punctuation.OpenBracket, 273 | Token.Keywords.Queries.Select, 274 | Token.Keywords.Queries.FieldName('LastName'), 275 | Token.Keywords.Queries.From, 276 | Token.Keywords.Queries.TypeName('Contact'), 277 | Token.Keywords.Queries.Where, 278 | Token.Keywords.Queries.FieldName('AccountId'), 279 | Token.Keywords.Queries.OperatorName('IN'), 280 | Token.Operators.Conditional.Colon, 281 | Token.Variables.Object('myObject'), 282 | Token.Operators.SafeNavigation, 283 | Token.Identifiers.MethodName('keys'), 284 | Token.Punctuation.OpenParen, 285 | Token.Punctuation.String.Begin, 286 | Token.XmlDocComments.String.SingleQuoted.Text('w'), 287 | Token.Punctuation.String.End, 288 | Token.Punctuation.CloseParen, 289 | Token.Punctuation.CloseBracket, 290 | Token.Punctuation.Semicolon, 291 | ]); 292 | }); 293 | }); 294 | }); 295 | --------------------------------------------------------------------------------