├── .gitignore ├── src ├── lang-server │ ├── .gitignore │ ├── .vscode │ │ ├── settings.json │ │ ├── extensions.json │ │ ├── tasks.json │ │ └── launch.json │ ├── client │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── src │ │ │ ├── coldfusionMappingsScript.ts │ │ │ └── extension.ts │ │ └── package-lock.json │ ├── tsconfig.json │ ├── .vscodeignore │ └── server │ │ ├── tsconfig.json │ │ ├── package.json │ │ ├── package-lock.json │ │ └── src │ │ ├── vscode-direct-adapter.ts │ │ ├── vscode-adapter.ts │ │ └── server.ts ├── services │ ├── buildShim.ts │ ├── clientAdapter.ts │ ├── tagnames.ts │ ├── cflsTypes.ts │ ├── tsconfig.json │ ├── languageService.ts │ ├── languageTool.ts │ └── completions.ts ├── compiler │ ├── index.ts │ ├── cancellationToken.ts │ ├── engines.ts │ └── tsconfig.json ├── build │ ├── utils.ts │ ├── build.ts │ └── tsconfig.json ├── grammar │ └── license-trail.txt └── scratch │ ├── tsconfig.json │ └── scratch.ts ├── test ├── sourcefiles │ ├── cfc_function_completion.cfm │ ├── for-in-completion.cfm │ ├── non-composite-function-type-annotation.cfc │ ├── inferredStructReturn.cfc │ ├── cfc_function_completion.cfc │ ├── tree-flatten-1.cfm │ ├── returnTypeCompletions.cfm │ └── arguments_lookup.cfc ├── binary-search.spec.ts ├── TestLoader.ts ├── tsconfig.json └── mxunit-smoketest.spec.ts ├── cflsp-vscode ├── whatisit.png ├── README.md ├── language-configuration.json └── package.json ├── .gitmodules ├── package.json ├── readme.md ├── tsconfig.json └── .vscode └── launch.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | out 3 | tsconfig.tsbuildinfo 4 | *.vsix 5 | 6 | -------------------------------------------------------------------------------- /src/lang-server/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | client/server 4 | .vscode-test -------------------------------------------------------------------------------- /test/sourcefiles/cfc_function_completion.cfm: -------------------------------------------------------------------------------- 1 | 2 | function foo(required y) { 3 | for (var somevar in y) { 4 | some|<<<< 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/services/buildShim.ts: -------------------------------------------------------------------------------- 1 | export interface IREPLACED_AT_BUILD { 2 | ClientAdapterModule_StaticRequirePath: string, 3 | runtimeLanguageToolPath: string, 4 | debug: boolean, 5 | }; 6 | -------------------------------------------------------------------------------- /src/lang-server/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false, 3 | "tslint.enable": true, 4 | "typescript.tsc.autoDetect": "off", 5 | "typescript.preferences.quoteStyle": "single", 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.eslint": true 8 | } 9 | } -------------------------------------------------------------------------------- /test/sourcefiles/non-composite-function-type-annotation.cfc: -------------------------------------------------------------------------------- 1 | /** 2 | * @!typedef SomeType = {foo: {aMember: string[], bar: string}} 3 | */ 4 | component { 5 | // @!arg targetArg : SomeType 6 | function foo(firstArg, targetArg, someOtherArg) { 7 | targetArg.foo.|<<<< 8 | } 9 | } -------------------------------------------------------------------------------- /test/sourcefiles/inferredStructReturn.cfc: -------------------------------------------------------------------------------- 1 | component { 2 | function foo() { 3 | var inner = () => { 4 | return { 5 | a: [{innerA: ""}], 6 | b: [{innerB: ""}], 7 | c: [{innerC: ""}] 8 | } 9 | } 10 | 11 | inner().b[999].|<<<< 12 | } 13 | } -------------------------------------------------------------------------------- /src/lang-server/.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": [ 7 | "dbaeumer.vscode-eslint" 8 | ] 9 | } -------------------------------------------------------------------------------- /test/sourcefiles/cfc_function_completion.cfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | function foo() { 4 | return 42; 5 | } 6 | 7 | function bar() { 8 | f|<<<< // at the moment, completions will return `foo` and `bar` from this position, and the client filters `bar` away 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/lang-server/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "moduleResolution": "node", 6 | "outDir": "../../../out/lang-server/client/", 7 | "sourceMap": true, 8 | "composite": true, 9 | "baseUrl": "./" 10 | }, 11 | "files": [ 12 | "src/extension.ts", 13 | "src/coldfusionMappingsScript.ts" 14 | ], 15 | } 16 | -------------------------------------------------------------------------------- /src/lang-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "lib": ["ES2019"], 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "sourceMap": true 9 | }, 10 | "include": [ 11 | "src" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test" 16 | ], 17 | "references": [ 18 | { "path": "./client" }, 19 | { "path": "./server" } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/lang-server/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | **/*.ts 3 | **/*.map 4 | .gitignore 5 | **/tsconfig.json 6 | **/tsconfig.base.json 7 | contributing.md 8 | .travis.yml 9 | client/node_modules/** 10 | !client/node_modules/vscode-jsonrpc/** 11 | !client/node_modules/vscode-languageclient/** 12 | !client/node_modules/vscode-languageserver-protocol/** 13 | !client/node_modules/vscode-languageserver-types/** 14 | !client/node_modules/semver/** 15 | -------------------------------------------------------------------------------- /test/sourcefiles/tree-flatten-1.cfm: -------------------------------------------------------------------------------- 1 | 7 | 8 | some_f|<<<< 9 | function some_func(required y) { 10 | for (var somevar in y) {} 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/lang-server/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["es2020"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "strict": true, 9 | "outDir": "../../../out/lang-server/server/", 10 | "composite": true, 11 | "baseUrl": "./", 12 | "paths": { 13 | "compiler": ["../../compiler"] 14 | } 15 | }, 16 | "files": ["src/server.ts", "src/vscode-adapter.ts", "src/vscode-direct-adapter.ts"], 17 | "references": [ 18 | {"path": "../../services"} 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/lang-server/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-server", 3 | "description": "Example implementation of a language server in node.", 4 | "version": "1.0.0", 5 | "author": "Microsoft Corporation", 6 | "license": "MIT", 7 | "engines": { 8 | "node": "*" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Microsoft/vscode-extension-samples" 13 | }, 14 | "dependencies": { 15 | "vscode-languageserver": "^7.0.0", 16 | "vscode-languageserver-textdocument": "^1.0.1", 17 | "vscode-uri": "^3.0.2" 18 | }, 19 | "scripts": {} 20 | } 21 | -------------------------------------------------------------------------------- /src/lang-server/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-client", 3 | "description": "VSCode part of a language server", 4 | "author": "Microsoft Corporation", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "vscode", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/Microsoft/vscode-extension-samples" 11 | }, 12 | "engines": { 13 | "vscode": "^1.52.0" 14 | }, 15 | "dependencies": { 16 | "vscode-languageclient": "^7.0.0", 17 | "vscode-uri": "^3.0.2" 18 | }, 19 | "devDependencies": { 20 | "@types/vscode": "^1.52.0", 21 | "vscode-test": "^1.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lang-server/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": [ 13 | "$tsc" 14 | ] 15 | }, 16 | { 17 | "type": "npm", 18 | "script": "watch", 19 | "isBackground": true, 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | }, 24 | "presentation": { 25 | "panel": "dedicated", 26 | "reveal": "never" 27 | }, 28 | "problemMatcher": [ 29 | "$tsc-watch" 30 | ] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /src/compiler/index.ts: -------------------------------------------------------------------------------- 1 | export { Scanner, CfFileType } from "./scanner"; 2 | export { Parser } from "./parser"; 3 | export { Binder } from "./binder"; 4 | export { Checker } from "./checker"; 5 | export { cfmOrCfc, flattenTree, binarySearch } from "./utils"; 6 | export { Node, SourceFile, Diagnostic, NilDCfm, NilCfc, NilCfm } from "./node"; 7 | 8 | export { DebugFileSystem, FileSystem, Project } from "./project"; 9 | 10 | // exports that we'd rather put wrappers on, in the form of services like "getCompletion" or etc. 11 | export { getNearestEnclosingScope, isExpressionContext, getTriviallyComputableString } from "./utils"; 12 | export { isStaticallyKnownScopeName, NodeId, NodeKind } from "./node"; -------------------------------------------------------------------------------- /cflsp-vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": [ "/*", "*/" ] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | ["{", "}"], 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["\"", "\""], 16 | ["'", "'"], 17 | ["#", "#"] 18 | ], 19 | "surroundingPairs": [ 20 | ["{", "}"], 21 | ["[", "]"], 22 | ["(", ")"], 23 | ["\"", "\""], 24 | ["'", "'"], 25 | ["#", "#"] 26 | ], 27 | "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\-`~!@#%\\^&*()=+[{\\]}\\\\|;:'\",.<>/?\\s]+)", 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cfc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "install-all": "npm install && cd src/lang-server/client && npm install && cd ../server && npm install && cd ../../../", 8 | "test-all": "tsc --build src/compiler && mocha --extension=ts --require=ts-node/register --spec=test/**/*.ts", 9 | "test-one": "tsc --build src/compiler && echo && mocha --extension=ts --require=ts-node/register --spec=", 10 | "clean": "rm -rf out && rm -rf cflsp-vscode/out", 11 | "build-cflsp": "tsc --build src/build && node out/build/build --debug", 12 | "build-cflsp-prod": "tsc --build src/build && node out/build/build" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@types/mocha": "^8.2.3", 18 | "@types/node": "^14.17.17", 19 | "esbuild": "^0.17.5", 20 | "mocha": "^8.4.0", 21 | "ts-node": "^9.1.1", 22 | "typescript": "^5.1.6" 23 | }, 24 | "dependencies": { 25 | "vscode-languageserver-textdocument": "^1.0.8" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/services/clientAdapter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps our internal representation of text and diagnostic ranges and etc. to some client's expecations about 3 | * how those things should be represented 4 | * 5 | * this is a "module interface" 6 | * - unenforceable but required: 7 | * an "implementing module" must export the single constant value `adapter : ClientAdapter` 8 | * it is then `require("path-to-impl").adapter`'d where it is needed 9 | */ 10 | 11 | import { SourceRange } from "../compiler/scanner" 12 | import { Diagnostic } from "../compiler/node"; 13 | import { CompletionItem } from "../services/completions"; 14 | import { AbsPath } from "../compiler/utils"; 15 | 16 | export type PosMapper = (pos: number) => T 17 | 18 | export interface ClientAdapter { 19 | diagnostic: (posMapper: PosMapper, diagnostic: Diagnostic) => unknown 20 | completionItem: (posMapper: PosMapper, completionItem: CompletionItem) => unknown 21 | sourceLocation: (posMapper: PosMapper, fsPath: AbsPath, sourceRange: SourceRange) => unknown 22 | } 23 | -------------------------------------------------------------------------------- /test/sourcefiles/returnTypeCompletions.cfm: -------------------------------------------------------------------------------- 1 | //// @file="/RootSibling.cfc" 2 | component { 3 | public function someRootSiblingMethod() {} 4 | } 5 | 6 | //// @file="/Top.cfc" 7 | component { 8 | public RootSibling function shouldReturnRootSibling() {} 9 | } 10 | 11 | //// @file="/Base.cfc" 12 | component extends="Top" { 13 | public function someBaseMethod() {} 14 | } 15 | 16 | //// @file="/foo/Child.cfc" 17 | component extends="Base" { 18 | public function someChildMethod() { 19 | shouldReturnRootSibling().|<<<< 20 | } 21 | private function shouldBeNotExportedBecauseItIsPrivate() {} 22 | } 23 | 24 | //// @file="/foo/Child2.cfc" 25 | component extends="Base" { 26 | public function someChildMethod() { 27 | so|<<<< // should get someChildMethod, shouldBeNotExportedBecauseItIsPrivate, Base::someBaseMethod, Top::shouldReturnRootSibling 28 | } 29 | private function shouldBeNotExportedBecauseItIsPrivate() {} 30 | } 31 | 32 | //// @file="/foo/Impl.cfm" 33 | 34 | new Child().|<<<< // should get Child::someChildMethod, Child::Base::someBaseMethod, Child::Base::Top::shouldReturnRootSibling 35 | -------------------------------------------------------------------------------- /test/sourcefiles/arguments_lookup.cfc: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | function foo(haha, ok) { 26 | arguments.|<<<< 27 | for (var x = 0; x < 10; x++) { 28 | 29 | } 30 | } 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/lang-server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "type": "extensionHost", 7 | "request": "launch", 8 | "name": "Launch Client", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "outFiles": ["${workspaceRoot}/client/out/**/*.js"], 12 | "preLaunchTask": { 13 | "type": "npm", 14 | "script": "watch" 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "attach", 20 | "name": "Attach to Server", 21 | "port": 6009, 22 | "restart": true, 23 | "outFiles": ["${workspaceRoot}/server/out/**/*.js"] 24 | }, 25 | { 26 | "name": "Language Server E2E Test", 27 | "type": "extensionHost", 28 | "request": "launch", 29 | "runtimeExecutable": "${execPath}", 30 | "args": [ 31 | "--extensionDevelopmentPath=${workspaceRoot}", 32 | "--extensionTestsPath=${workspaceRoot}/client/out/test/index", 33 | "${workspaceRoot}/client/testFixture" 34 | ], 35 | "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"] 36 | } 37 | ], 38 | "compounds": [ 39 | { 40 | "name": "Client + Server", 41 | "configurations": ["Launch Client", "Attach to Server"] 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cfc 2 | A compiler for Coldfusion, designed primarily for editor tooling support. 3 | 4 | The VsCode plugin based on this tool is "cflsp", downloadable from within VsCode or on the [extensions marketplace](https://marketplace.visualstudio.com/items?itemName=DavidRogers.cflsp) 5 | 6 | ## current goals 7 | * Support ColdFusion 2018+ and Lucee5+ 8 | * Recognize syntactic and some semantic errors and emit reasonable diagnostics for them. 9 | * Parse incrementally to keep response times reasonable 10 | * Offer autocomplete and navigate-to-symbol where possible 11 | 12 | ## building 13 | 14 | * `git clone https://github.com/softwareCobbler/cfc.git && cd cfc` 15 | * `npm run install-all` 16 | * `npm run build-cflsp` 17 | 18 | Then, from within vscode, there is a debug configuration called "Client + Server" which launches the VSCode extension host with the built plugin running, as well as a debug connection to the server. The actual language server glue code is about 99% pulled from [Microsoft's vs-code-extension-samples/lsp-sample](https://github.com/microsoft/vscode-extension-samples/tree/main/lsp-sample), but the parser is all handrolled recursive descent. 19 | 20 | ## tests 21 | to run the current tests (just a quick smoke test, hopefully helps check for regressions during development), install the git submodules: 22 | * `git submodule init` 23 | * `git submodule update` 24 | 25 | Now the directory `test/mxunit` should be populated, and the test can run: 26 | * `npm run test-all` 27 | -------------------------------------------------------------------------------- /test/binary-search.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { binarySearch } from "../out/compiler"; 3 | 4 | function find(ns: number[], target: number) { 5 | const result = binarySearch(ns, (v) => v < target ? -1 : v === target ? 0 : 1); 6 | return result; 7 | } 8 | 9 | describe("Binary search works", () => { 10 | it("should find the thing", () => { 11 | const things = [1,3,5]; 12 | assert.strictEqual(find(things, 1), 0); 13 | assert.strictEqual(find(things, 3), 1); 14 | assert.strictEqual(find(things, 5), 2); 15 | assert.strictEqual(find(things, 0), ~0); 16 | assert.strictEqual(find(things, 7), ~(things.length-1)); 17 | }); 18 | it("should find the thing", () => { 19 | const things = [1,3,5,7]; 20 | assert.strictEqual(find(things, 1), 0); 21 | assert.strictEqual(find(things, 3), 1); 22 | assert.strictEqual(find(things, 5), 2); 23 | assert.strictEqual(find(things, 7), 3); 24 | assert.strictEqual(find(things, 0), ~0); 25 | assert.strictEqual(find(things, 9), ~(things.length-1)); 26 | }); 27 | it("should find the thing", () => { 28 | const things = [1]; 29 | assert.strictEqual(find(things, 1), 0); 30 | assert.strictEqual(find(things, 0), ~0); 31 | assert.strictEqual(find(things, 7), ~(things.length-1)); 32 | }); 33 | it("should find the thing", () => { 34 | const things : number[] = []; 35 | assert.strictEqual(find(things, 1), ~0); 36 | assert.strictEqual(find(things, 7), ~0); 37 | }); 38 | }) 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, /* Enable incremental compilation */ 4 | "composite": true, 5 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 7 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 8 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 9 | "sourceMap": true, /* Generates corresponding '.map' file. */ 10 | 11 | "strict": true, /* Enable all strict type-checking options. */ 12 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 13 | "strictNullChecks": true, /* Enable strict null checks. */ 14 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 15 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 16 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 17 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 18 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 19 | 20 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 21 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 22 | }, 23 | "references": [ 24 | {"path": "./src/compiler"}, 25 | {"path": "./src/services"}, 26 | {"path": "./src/lang-server/client"}, 27 | {"path": "./src/lang-server/server"} 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /src/build/utils.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from "child_process"; 2 | import * as esbuild from "esbuild"; 3 | 4 | interface TighterBuildOptions extends esbuild.BuildOptions { 5 | entryPoints: [string], // is there a way to say here "entryPoints as a property name must be a property name on BuildOptions", e.g. "override" or etc. 6 | outfile: string 7 | } 8 | 9 | export function esBuildOrFail(options: esbuild.BuildOptions & TighterBuildOptions) : void { 10 | console.log(`[esbuild] bundling entrypoint ${options.entryPoints[0]}`); 11 | try { 12 | esbuild.buildSync(options); 13 | } 14 | catch (e) { 15 | process.exit(); 16 | } 17 | } 18 | 19 | export function tscBuildOrFail(tscCmd: string, buildTarget: string) : void { 20 | console.log(`[tsc] building ${buildTarget}`); 21 | const tscOutputLines = fixupTscOutput(child_process.spawnSync(tscCmd, ["--build", buildTarget])); 22 | if (tscOutputLines.length > 0) { 23 | for (const msg of tscOutputLines) { 24 | console.log(msg); 25 | } 26 | process.exit(); 27 | } 28 | } 29 | 30 | // child_process.spawnSync().output effectively returns (null | string)[], 31 | // we want the non-null, non-empty-string elements 32 | function fixupTscOutput(childProcessResult: child_process.SpawnSyncReturns) : string[] { 33 | return filterMap( 34 | childProcessResult.output, 35 | null, 36 | (line) => line ? (line.toString() || null) : null); 37 | } 38 | 39 | // not strictly necessary, messing with typescript 40 | // trying to get a well-typed O(n) instead of O(2n) "filter + map" 41 | type Subtract = TUnionMinuend extends TSubtrahend ? never : TUnionMinuend; 42 | function filterMap(ts: T[], nilIdentity: NilMarker, f: (t: T) => U | NilMarker) : Subtract[] { 43 | const result : U[] = []; 44 | for (const t of ts) { 45 | const r = f(t); 46 | if (r === nilIdentity) continue; 47 | else result.push(r as U); 48 | } 49 | return result as any; 50 | } -------------------------------------------------------------------------------- /src/lang-server/server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "vscode-jsonrpc": { 8 | "version": "6.0.0", 9 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 10 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" 11 | }, 12 | "vscode-languageserver": { 13 | "version": "7.0.0", 14 | "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", 15 | "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", 16 | "requires": { 17 | "vscode-languageserver-protocol": "3.16.0" 18 | } 19 | }, 20 | "vscode-languageserver-protocol": { 21 | "version": "3.16.0", 22 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 23 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 24 | "requires": { 25 | "vscode-jsonrpc": "6.0.0", 26 | "vscode-languageserver-types": "3.16.0" 27 | } 28 | }, 29 | "vscode-languageserver-textdocument": { 30 | "version": "1.0.1", 31 | "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz", 32 | "integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA==" 33 | }, 34 | "vscode-languageserver-types": { 35 | "version": "3.16.0", 36 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 37 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 38 | }, 39 | "vscode-uri": { 40 | "version": "3.0.2", 41 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.2.tgz", 42 | "integrity": "sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==" 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /test/TestLoader.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const completionMarkerPattern = /\|<<< 0) { 47 | throw "expected only one completions marker"; 48 | } 49 | matchIndex = match.index; 50 | sourceText = sourceText.slice(0, matchIndex) + sourceText.slice(matchIndex + match[0].length); // "x.|<<< 3 | // 4 | // copy and paste this to some server path you want the mappings for 5 | // 6 | // this will dump out the serialized JSON of cf mappings and wirebox mappings 7 | // in the format expected by cfls 8 | // click the
 element that gets rendered in the browser to copy the resulting text
 9 | 	// then save that text in \${editorWorkspaceRoot}/cfconfig.json
10 | 	//
11 | 
12 | 	function getCfMappings() {
13 | 		var cfMappings = getApplicationMetadata().mappings;
14 | 		var result = {};
15 | 		var appRoot = expandPath("");
16 | 
17 | 		for (var key in cfMappings) {
18 | 			var mapping_stripLeadingPathSep = reReplace(regex="^[\\\\/]", scope="one", string=key, substring="");
19 | 			var mapping_replacePathSepsWithDots = reReplace(regex="[\\\\/]", scope="all", string=mapping_stripLeadingPathSep, substring=".");
20 | 
21 | 			var target_stripRootPrefix = replace(string=cfMappings[key], scope="one", substring1=appRoot, substring2="");
22 | 			var target_normalized = reReplace(regex="[\\\\/]", scope="all", string=target_stripRootPrefix, substring="/");
23 | 			result[mapping_replacePathSepsWithDots] = target_normalized;
24 | 		}
25 | 		writedump(result)
26 | 		return result;
27 | 	}
28 | 
29 | 	// pass in a ref to your wirebox instance
30 | 	function getWireboxMappings(wirebox) {
31 | 		var mappings = wirebox.getBinder().getMappings();
32 | 		var result = {};
33 | 		for (var name in mappings.keyArray()) {
34 | 			var path = mappings[name].getPath();
35 | 			if (isSimpleValue(path)) { // sometimes it's a function?
36 | 				result[name] = path;
37 | 			}
38 | 		}
39 | 		return result;
40 | 	}
41 | 
42 | 	writeoutput("
43 | 		
44 | 		
45 | 57 | ") 58 | `; 59 | -------------------------------------------------------------------------------- /src/compiler/cancellationToken.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import * as net from "net"; 3 | import * as fs from "fs"; 4 | 5 | export function CancellationToken() { 6 | function randomName() { 7 | return randomBytes(16).toString("hex"); 8 | } 9 | 10 | const id = `\\\\.\\pipe\\cfls-token-${randomName()}`; 11 | 12 | // 13 | // ENGAGED = true if the socket is open 14 | // - "this socket is listening" 15 | // - "or we've asked it to and it could start at any time" 16 | // - "or we've asked it to close but it hasn't yet" 17 | // 18 | // Plenty of EADDRINUSE if we try to open a socket on the same name 19 | // 20 | let ENGAGED = false; 21 | let socket = net.createServer(); 22 | 23 | socket.on("close", () => { 24 | // it can take a few microseconds between `socket.close()` and the actual close event 25 | // during which time, the OS (or at least Windows) will say that 26 | // - socket.listening is false, 27 | // - but socket.listen(id) is an error because it really actually is still listening 28 | // 29 | ENGAGED = false; 30 | }) 31 | 32 | socket.on("error", (_err) => { 33 | // almost certainly EADDRINUSE 34 | // which is thrown asynchronously, 35 | // so registering this listener acts as a catch block 36 | // 37 | // can put a breakpoint here if necessary, 38 | // otherwise this is a no-op 39 | }) 40 | 41 | function requestCancellation() { 42 | if (ENGAGED) { 43 | return; 44 | } 45 | else { 46 | socket.listen(id); 47 | ENGAGED = true; 48 | } 49 | } 50 | 51 | function reset() { 52 | socket.close(); 53 | } 54 | 55 | return { 56 | requestCancellation, 57 | reset, 58 | getId: () => id 59 | } 60 | } 61 | export type CancellationToken = ReturnType; 62 | 63 | export class CancellationException { 64 | name : string; 65 | constructor() { 66 | this.name = "CancellationException"; // for debugability, generally want to NOT break on these 67 | }; 68 | } 69 | 70 | export function CancellationTokenConsumer(cancellationTokenId: string) { 71 | function cancellationRequested() { 72 | return fs.existsSync(cancellationTokenId); 73 | } 74 | function throwIfCancellationRequested() { 75 | if (cancellationRequested()) { 76 | throw new CancellationException(); 77 | } 78 | } 79 | return { 80 | cancellationRequested, 81 | throwIfCancellationRequested 82 | } 83 | } 84 | 85 | export type CancellationTokenConsumer = ReturnType; -------------------------------------------------------------------------------- /src/build/build.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | import * as esbuild from "esbuild"; 5 | 6 | import { tscBuildOrFail, esBuildOrFail } from "./utils"; 7 | 8 | // 9 | // general config 10 | // 11 | 12 | const debug = process.argv.indexOf("--debug") !== -1; 13 | const tscCmd = process.platform === "win32" ? "tsc.cmd" : "tsc"; 14 | 15 | const commonEsBuildOptions : esbuild.BuildOptions = { 16 | bundle: true, 17 | platform: "node", 18 | format: "cjs", 19 | external: ["vscode"], 20 | define: { 21 | "REPLACED_AT_BUILD.ClientAdapterModule_StaticRequirePath": `"../lang-server/server/src/vscode-adapter"`, 22 | "REPLACED_AT_BUILD.runtimeLanguageToolPath": `"./languageTool.js"`, 23 | "REPLACED_AT_BUILD.debug": debug ? "true" : "false", 24 | }, 25 | sourcemap: true 26 | } 27 | 28 | doTsc(); 29 | doEsBuild(); 30 | doResources(); 31 | 32 | function doTsc() { 33 | tscBuildOrFail(tscCmd, "src/compiler"); 34 | tscBuildOrFail(tscCmd, "src/services"); 35 | tscBuildOrFail(tscCmd, "src/lang-server"); 36 | } 37 | 38 | function doEsBuild() { 39 | // actual language tool 40 | // should run in its own process because it will block on long parses or etc. 41 | esBuildOrFail({ 42 | ...commonEsBuildOptions, 43 | entryPoints: ["./src/services/languageTool.ts"], 44 | outfile: "./cflsp-vscode/out/languageTool.js", 45 | }); 46 | 47 | // "server frontend", handles LSP and asks language tool for diagnostics and etc. 48 | // currently this is really "the vscode server" 49 | esBuildOrFail({ 50 | ...commonEsBuildOptions, 51 | entryPoints: ["./src/lang-server/server/src/server.ts"], 52 | outfile: "./cflsp-vscode/out/server.js", 53 | }); 54 | 55 | // vscode extension entry point, kicks off the "server frontend" in its own process 56 | // vscode manages the LSP here, crafting requests as a result of user interactions with the editor 57 | // and interpreting the responses of the server as "put squiggly under these characters" or etc. 58 | esBuildOrFail({ 59 | ...commonEsBuildOptions, 60 | entryPoints: ["./src/lang-server/client/src/extension.ts"], 61 | outfile: "./cflsp-vscode/out/extension.js", 62 | }); 63 | } 64 | 65 | function doResources() { 66 | function fromTo(from: string, to: string) { 67 | console.log(`[copying] '${from}' -> '${to}'`) 68 | fs.copyFileSync(from, to); 69 | } 70 | fromTo(path.resolve("src/lang-server/server/src/runtimelib/lib.cf2018.d.cfm"), path.resolve("cflsp-vscode/out/lib.cf2018.d.cfm")); 71 | fromTo(path.resolve("src/grammar/grammar.json"), path.resolve("cflsp-vscode/out/grammar.json")); 72 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "test scratchpad", 11 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 12 | "program": "${workspaceFolder}/out/scratch/scratch.js" 13 | }, 14 | { 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "name": "Launch cflsp (client plugin)", 18 | "runtimeExecutable": "${execPath}", 19 | "args": ["--extensionDevelopmentPath=${workspaceFolder}/cflsp-vscode/"], 20 | "outFiles": ["${workspaceFolder}/cflsp-vscode/out/**/*.js"], 21 | "trace": true, 22 | "skipFiles": [ 23 | "/**", 24 | "/snap/**", 25 | "/home/david/.vscode/**" 26 | ] 27 | }, 28 | { 29 | "type": "node", 30 | "request": "attach", 31 | "name": "Attach to vs-code server process", 32 | "port": 6011, 33 | "outFiles": ["${workspaceFolder}/cflsp-vscode/out/**/*.js"], 34 | "trace": true, 35 | }, 36 | { 37 | "type": "node", 38 | "request": "attach", 39 | "name": "Attach to parse/bind/check process", 40 | "port": 6012, 41 | "outFiles": ["${workspaceFolder}/cflsp-vscode/out/**/*.js"], 42 | "trace": true, 43 | "skipFiles": [ 44 | "/**", 45 | "/snap/**", 46 | "/home/david/.vscode/**" 47 | ] 48 | }, 49 | { 50 | "name": "Debug current test", 51 | "type": "node", 52 | "request": "launch", 53 | "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", 54 | "stopOnEntry": false, 55 | "args": ["--spec=${file}", "--extension=ts", "--require=ts-node/register", "--no-timeouts"], 56 | "cwd": "${workspaceFolder}", 57 | "runtimeExecutable": null, 58 | "sourceMaps": true, 59 | "env": { "NODE_ENV": "testing"} 60 | }, 61 | { 62 | "name": "Debug build system", 63 | "autoAttachChildProcesses": false, 64 | "type": "node", 65 | "request": "launch", 66 | "program": "${workspaceFolder}/out/build/build.js", 67 | "cwd": "${workspaceFolder}", 68 | "sourceMaps": true, 69 | "skipFiles": [ 70 | "/**", 71 | ], 72 | "outFiles": [ 73 | "${workspaceFolder}/out/**/*.js", 74 | ] 75 | } 76 | ], 77 | "compounds": [ 78 | { 79 | "name": "Client + cflsServer", 80 | "configurations": [ 81 | "Launch cflsp (client plugin)", 82 | "Attach to parse/bind/check process", 83 | ] 84 | }, 85 | { 86 | "name": "all", 87 | "configurations": [ 88 | "Launch cflsp (client plugin)", 89 | "Attach to parse/bind/check process", 90 | "Attach to vs-code server process", 91 | ] 92 | } 93 | ] 94 | } -------------------------------------------------------------------------------- /src/grammar/license-trail.txt: -------------------------------------------------------------------------------- 1 | the grammar file in this directory, `grammar.json`, looks like it has been hopping many projects: 2 | 3 | in order of oldest to newest: 4 | a) https://github.com/textmate/coldfusion.tmbundle 5 | b) https://github.com/ilich/vscode-coldfusion/ 6 | c) https://github.com/KamasamaK/vscode-cfml 7 | 8 | @@ license from a 9 | If not otherwise specified (see below), files in this repository fall under the following license: 10 | 11 | Permission to copy, use, modify, sell and distribute this 12 | software is granted. This software is provided "as is" without 13 | express or implied warranty, and with no claim as to its 14 | suitability for any purpose. 15 | 16 | @@ license from b 17 | The MIT License (MIT) 18 | 19 | Copyright (c) 2015 Ilya Verbitskiy 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy 22 | of this software and associated documentation files (the "Software"), to deal 23 | in the Software without restriction, including without limitation the rights 24 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 25 | copies of the Software, and to permit persons to whom the Software is 26 | furnished to do so, subject to the following conditions: 27 | 28 | The above copyright notice and this permission notice shall be included in all 29 | copies or substantial portions of the Software. 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | 39 | @@ license from c 40 | MIT License 41 | 42 | Copyright (c) 2017 KamasamaK 43 | 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | 51 | The above copyright notice and this permission notice shall be included in all 52 | copies or substantial portions of the Software. 53 | 54 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 55 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 56 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 57 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 58 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 59 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 60 | SOFTWARE. -------------------------------------------------------------------------------- /src/services/tagnames.ts: -------------------------------------------------------------------------------- 1 | export const tagNames = [ 2 | "abort", 3 | "adminsecurity", 4 | "ajaximport", 5 | "ajaxproxy", 6 | "applet", 7 | "application", 8 | "associate", 9 | "break", 10 | "cache", 11 | "case", 12 | "chart", 13 | "chartdata", 14 | "chartseries", 15 | "clientsettings", 16 | "col", 17 | "collection", 18 | "content", 19 | "continue", 20 | "cookie", 21 | "dbinfo", 22 | "directory", 23 | "div", 24 | "document", 25 | "documentitem", 26 | "documentsection", 27 | "dump", 28 | "else", 29 | "elseif", 30 | "error", 31 | "exchangecalendar", 32 | "exchangeconnection", 33 | "exchangecontact", 34 | "exchangeconversation", 35 | "exchangefilter", 36 | "exchangefolder", 37 | "exchangemail", 38 | "exchangetask", 39 | "execute", 40 | "exit", 41 | "feed", 42 | "file", 43 | "fileupload", 44 | "flush", 45 | "form", 46 | "formgroup", 47 | "formitem", 48 | "ftp", 49 | "function", 50 | "grid", 51 | "gridcolumn", 52 | "gridrow", 53 | "gridupdate", 54 | "header", 55 | "htmlhead", 56 | "htmltopdf", 57 | "htmltopdfitem", 58 | "http", 59 | "httpparam", 60 | "httppool", 61 | "if", 62 | "image", 63 | "imap", 64 | "imapfilter", 65 | "import", 66 | "include", 67 | "index", 68 | "input", 69 | "insert", 70 | "layout", 71 | "layoutarea", 72 | "ldap", 73 | "location", 74 | "lock", 75 | "log", 76 | "loop", 77 | "mail", 78 | "mailparam", 79 | "map", 80 | "mapitem", 81 | "mediaplayer", 82 | "menu", 83 | "menuitem", 84 | "messagebox", 85 | "module", 86 | "ntauthenticate", 87 | "oauth", 88 | "object", 89 | "objectcache", 90 | "output", 91 | "param", 92 | "pdf", 93 | "pdfform", 94 | "pdfformparam", 95 | "pdfparam", 96 | "pdfsubform", 97 | "pod", 98 | "pop", 99 | "popfilter", 100 | "presentation", 101 | "presentationslide", 102 | "presenter", 103 | "print", 104 | "processingdirective", 105 | "procparam", 106 | "procresult", 107 | "progressbar", 108 | "query", 109 | "queryparam", 110 | "registry", 111 | "report", 112 | "reportparam", 113 | "return", 114 | "savecontent", 115 | "schedule", 116 | "script", 117 | "search", 118 | "select", 119 | "set", 120 | "setting", 121 | "sharepoint", 122 | "silent", 123 | "slider", 124 | "spreadsheet", 125 | "sprydataset", 126 | "storedproc", 127 | "switch", 128 | "table", 129 | "textarea", 130 | "thread", 131 | "throw", 132 | "timer", 133 | "tooltip", 134 | "trace", 135 | "transaction", 136 | "tree", 137 | "treeitem", 138 | "update", 139 | "wddx", 140 | "websocket", 141 | "widget", 142 | "window", 143 | "zip", 144 | "zipparam", 145 | ] as const; -------------------------------------------------------------------------------- /src/lang-server/server/src/vscode-direct-adapter.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind as vsCompletionItemKind, CompletionItem as vsCompletionItem, Diagnostic as vsDiagnostic, DiagnosticSeverity as vsDiagnosticSeverity } from "vscode-languageserver/node"; 2 | import { Position as vsPosition, Range as vsRange, Location as vsLocation } from "vscode-languageserver-types" 3 | import { Diagnostic, DiagnosticKind, SourceFile } from "../../../compiler/node"; 4 | import { CompletionItemKind, CompletionItem } from "../../../services/completions"; 5 | import { SourceRange } from "../../../compiler/scanner" 6 | import { ClientAdapter, PosMapper } from "../../../services/clientAdapter"; 7 | import { AbsPath, exhaustiveCaseGuard } from "../../../compiler/utils"; 8 | import { URI } from "vscode-uri"; 9 | 10 | import { 11 | TextDocument 12 | } from 'vscode-languageserver-textdocument'; 13 | 14 | function cfPositionToVsPosition(doc: TextDocument) : (pos: number) => vsPosition { 15 | return (pos: number) => doc.positionAt(pos); 16 | } 17 | 18 | const nilRangeAsAnnotatedCharLike : vsPosition = {line: 0, character: 0} 19 | 20 | function cfRangeToVsRange(posMapper: PosMapper, sourceRange: SourceRange) : vsRange { 21 | const start = sourceRange.isNil() ? nilRangeAsAnnotatedCharLike : posMapper(sourceRange.fromInclusive); 22 | const end = sourceRange.isNil() ? nilRangeAsAnnotatedCharLike : posMapper(sourceRange.toExclusive); 23 | return { 24 | start, 25 | end 26 | } 27 | } 28 | 29 | function diagnostic(posMapper: PosMapper, diagnostic: Diagnostic) : vsDiagnostic { 30 | return { 31 | severity: diagnostic.kind === DiagnosticKind.error ? vsDiagnosticSeverity.Error : vsDiagnosticSeverity.Warning, 32 | range: { 33 | start: posMapper(diagnostic.fromInclusive), 34 | end: posMapper(diagnostic.toExclusive) 35 | }, 36 | message: diagnostic.msg, 37 | source: "cfls" 38 | } 39 | } 40 | 41 | function mapCflsCompletionItemKindToVsCodeCompletionItemKind(kind: CompletionItemKind) : vsCompletionItemKind { 42 | switch (kind) { 43 | case CompletionItemKind.function: return vsCompletionItemKind.Function; 44 | case CompletionItemKind.structMember: return vsCompletionItemKind.Field; 45 | case CompletionItemKind.tagName: return vsCompletionItemKind.Property; 46 | case CompletionItemKind.variable: return vsCompletionItemKind.Variable; 47 | case CompletionItemKind.stringLiteral: return vsCompletionItemKind.Constant; // not value 48 | default: exhaustiveCaseGuard(kind); 49 | } 50 | } 51 | 52 | function mapCflsCompletionToVsCodeCompletion(posMapper: PosMapper, completion: CompletionItem) : vsCompletionItem { 53 | const result : Partial = {}; 54 | result.label = completion.label; 55 | result.kind = mapCflsCompletionItemKindToVsCodeCompletionItemKind(completion.kind); 56 | if (completion.detail) result.detail = completion.detail; 57 | if (completion.insertText) result.insertText = completion.insertText; 58 | if (completion.sortText) result.sortText = completion.sortText; 59 | if (completion.textEdit) { 60 | result.textEdit = { 61 | insert: cfRangeToVsRange(posMapper, completion.textEdit.range), 62 | newText: completion.textEdit.newText, 63 | replace: cfRangeToVsRange(posMapper, completion.textEdit.replace), 64 | } 65 | } 66 | return result as vsCompletionItem; 67 | } 68 | 69 | function sourceLocation(posMapper: PosMapper, sourceLocation: {sourceFile: SourceFile, range: SourceRange}) : vsLocation { 70 | return { 71 | uri: URI.file(sourceLocation.sourceFile.absPath).toString(), 72 | range: cfRangeToVsRange(posMapper, sourceLocation.range) 73 | } 74 | } 75 | 76 | export const adapter = { 77 | cfPositionToVsPosition, 78 | diagnostic, 79 | completionItem: mapCflsCompletionToVsCodeCompletion, 80 | sourceLocation 81 | } 82 | -------------------------------------------------------------------------------- /src/lang-server/server/src/vscode-adapter.ts: -------------------------------------------------------------------------------- 1 | import { CompletionItemKind as vsCompletionItemKind, CompletionItem as vsCompletionItem, Diagnostic as vsDiagnostic, DiagnosticSeverity as vsDiagnosticSeverity } from "vscode-languageserver/node"; 2 | import { Position as vsPosition, Range as vsRange, Location as vsLocation } from "vscode-languageserver-types" 3 | import { Diagnostic, DiagnosticKind } from "../../../compiler/node"; 4 | import { CompletionItemKind, CompletionItem } from "../../../services/completions"; 5 | import { SourceRange } from "../../../compiler/scanner" 6 | import { ClientAdapter, PosMapper } from "../../../services/clientAdapter"; 7 | import { AbsPath, exhaustiveCaseGuard } from "../../../compiler/utils"; 8 | import { URI } from "vscode-uri"; 9 | 10 | interface LineCol { line: number, col: number } 11 | 12 | function cfPositionToVsPosition(posMapper: PosMapper, pos: number) : vsPosition { 13 | const annotatedChar = posMapper(pos); 14 | return { 15 | line: annotatedChar.line, 16 | character: annotatedChar.col 17 | } 18 | } 19 | 20 | const nilRangeAsAnnotatedCharLike = {line: 0, col: 0}; 21 | 22 | function cfRangeToVsRange(posMapper: PosMapper, sourceRange: SourceRange) : vsRange { 23 | const from = sourceRange.isNil() ? nilRangeAsAnnotatedCharLike : posMapper(sourceRange.fromInclusive); 24 | const to = sourceRange.isNil() ? nilRangeAsAnnotatedCharLike : posMapper(sourceRange.toExclusive); 25 | return { 26 | start: {line: from.line, character: from.col}, 27 | end: {line: to.line, character: to.col} 28 | } 29 | } 30 | 31 | function diagnostic(posMapper: PosMapper, diagnostic: Diagnostic) : vsDiagnostic { 32 | return { 33 | severity: diagnostic.kind === DiagnosticKind.error ? vsDiagnosticSeverity.Error : vsDiagnosticSeverity.Warning, 34 | range: { 35 | start: cfPositionToVsPosition(posMapper, diagnostic.fromInclusive), 36 | end: cfPositionToVsPosition(posMapper, diagnostic.toExclusive) 37 | }, 38 | message: diagnostic.msg, 39 | source: "cfls" 40 | } 41 | } 42 | 43 | function mapCflsCompletionItemKindToVsCodeCompletionItemKind(kind: CompletionItemKind) : vsCompletionItemKind { 44 | switch (kind) { 45 | case CompletionItemKind.function: return vsCompletionItemKind.Function; 46 | case CompletionItemKind.structMember: return vsCompletionItemKind.Field; 47 | case CompletionItemKind.tagName: return vsCompletionItemKind.Property; 48 | case CompletionItemKind.variable: return vsCompletionItemKind.Variable; 49 | case CompletionItemKind.stringLiteral: return vsCompletionItemKind.Constant; // not value 50 | default: exhaustiveCaseGuard(kind); 51 | } 52 | } 53 | 54 | function mapCflsCompletionToVsCodeCompletion(posMapper: PosMapper, completion: CompletionItem) : vsCompletionItem { 55 | const result : Partial = {}; 56 | result.label = completion.label; 57 | result.kind = mapCflsCompletionItemKindToVsCodeCompletionItemKind(completion.kind); 58 | if (completion.detail) result.detail = completion.detail; 59 | if (completion.insertText) result.insertText = completion.insertText; 60 | if (completion.sortText) result.sortText = completion.sortText; 61 | if (completion.textEdit) { 62 | result.textEdit = { 63 | insert: cfRangeToVsRange(posMapper, completion.textEdit.range), 64 | newText: completion.textEdit.newText, 65 | replace: cfRangeToVsRange(posMapper, completion.textEdit.replace), 66 | } 67 | } 68 | return result as vsCompletionItem; 69 | } 70 | 71 | function sourceLocation(posMapper: PosMapper, fsPath: AbsPath, sourceRange: SourceRange) : vsLocation { 72 | return { 73 | uri: URI.file(fsPath).toString(), 74 | range: cfRangeToVsRange(posMapper, sourceRange) 75 | } 76 | } 77 | 78 | export const adapter = { 79 | diagnostic, 80 | completionItem: mapCflsCompletionToVsCodeCompletion, 81 | sourceLocation 82 | } as const satisfies ClientAdapter -------------------------------------------------------------------------------- /src/lang-server/client/src/extension.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as path from 'path'; 7 | import * as fs from "fs"; 8 | import { window, ProgressLocation, workspace, ExtensionContext, commands, TextDocumentContentProvider, Uri } from 'vscode'; 9 | import { URI } from "vscode-uri"; 10 | 11 | import { 12 | LanguageClient, 13 | LanguageClientOptions, 14 | ServerOptions, 15 | TransportKind 16 | } from 'vscode-languageclient/node'; 17 | 18 | import { coldfusionMappingsScript } from "./coldfusionMappingsScript"; 19 | 20 | let client: LanguageClient; 21 | let libAbsPath : string | null = null; 22 | 23 | export function activate(context: ExtensionContext) { 24 | workspace.registerTextDocumentContentProvider("cfls", new (class implements TextDocumentContentProvider { 25 | provideTextDocumentContent(_uri: Uri): string { 26 | return coldfusionMappingsScript; 27 | } 28 | })()); 29 | 30 | commands.registerCommand("cflsp.generateColdfusionMappingsScript", async () => { 31 | const uri = URI.parse("cfls:coldFusionMappingsScript.cfm"); 32 | let doc = await workspace.openTextDocument(uri); 33 | await window.showTextDocument(doc, { preview: false }); 34 | }); 35 | 36 | libAbsPath = context.asAbsolutePath("./out/lib.cf2018.d.cfm"); 37 | 38 | // The server is implemented in node 39 | let serverModule = context.asAbsolutePath("./out/server.js"); 40 | 41 | // The debug options for the server 42 | // --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging 43 | let debugOptions = { execArgv: ['--nolazy', '--inspect=6011', /*'--inspect-brk'*/] }; 44 | 45 | // If the extension is launched in debug mode then the debug server options are used 46 | // Otherwise the run options are used 47 | let serverOptions: ServerOptions = { 48 | run: { module: serverModule, transport: TransportKind.ipc }, 49 | debug: { 50 | module: serverModule, 51 | transport: TransportKind.ipc, 52 | options: debugOptions 53 | }, 54 | }; 55 | 56 | // Options to control the language client 57 | let clientOptions: LanguageClientOptions = { 58 | // Register the server for plain text documents 59 | documentSelector: [ 60 | //{ scheme: 'file', language: 'plaintext' }, 61 | { scheme: 'file', language: 'cfml' } 62 | ], 63 | synchronize: { 64 | // Notify the server about file changes to '.clientrc files contained in the workspace 65 | fileEvents: workspace.createFileSystemWatcher('**/.clientrc') 66 | }, 67 | initializationOptions: { 68 | libAbsPath, 69 | config: workspace.getConfiguration("cflsp") 70 | } 71 | }; 72 | 73 | 74 | // Create the language client and start the client. 75 | client = new LanguageClient( 76 | "cflsp", 77 | "cflsp", 78 | serverOptions, 79 | clientOptions 80 | ); 81 | 82 | 83 | // Start the client. This will also launch the server 84 | client.start(); 85 | } 86 | 87 | function recursiveGetFiles(root: string, pattern: RegExp) : string [] { 88 | const result : string[] = []; 89 | const fds = fs.readdirSync(root, {withFileTypes: true}); 90 | for (const fd of fds) { 91 | if (fd.isDirectory()) result.push(...recursiveGetFiles(path.resolve(root, fd.name), pattern)); 92 | else if (pattern.test(fd.name)) { 93 | const fspath = path.resolve(root, fd.name); 94 | result.push(fspath); 95 | } 96 | } 97 | return result; 98 | } 99 | 100 | export function deactivate(): Thenable | undefined { 101 | if (!client) { 102 | return undefined; 103 | } 104 | return client.stop(); 105 | } 106 | -------------------------------------------------------------------------------- /cflsp-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cflsp", 3 | "description": "ColdFusion syntax error checker", 4 | "author": "David Rogers", 5 | "license": "MIT", 6 | "version": "1.0.51", 7 | "publisher": "DavidRogers", 8 | "categories": [], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/softwareCobbler/cfc.git" 12 | }, 13 | "keywords": [ 14 | "ColdFusion" 15 | ], 16 | "engines": { 17 | "vscode": "^1.43.0" 18 | }, 19 | "activationEvents": [ 20 | "onLanguage:cfml" 21 | ], 22 | "main": "./out/extension.js", 23 | "files": [ 24 | "out/server.js", 25 | "out/languageTool.js", 26 | "out/lib.cf2018.d.cfm" 27 | ], 28 | "contributes": { 29 | "commands": [ 30 | {"command": "cflsp.generateColdfusionMappingsScript", "title": "Generate ColdFusion mappings script", "enablement": ""} 31 | ], 32 | "languages": [ 33 | { 34 | "id": "cfml", 35 | "extensions": [".cfm", ".cfc", ".cfml"], 36 | "aliases": ["ColdFusion", "CFML"], 37 | "filenames": [], 38 | "configuration": "./language-configuration.json" 39 | } 40 | ], 41 | "grammars": [ 42 | { 43 | "language": "cfml", 44 | "scopeName": "embedding.cfml", 45 | "path": "./out/grammar.json", 46 | "embeddedLanguages": { 47 | "source.css": "css", 48 | "meta.embedded.block.css": "css", 49 | "source.js": "javascript", 50 | "meta.embedded.block.js": "javascript", 51 | "meta.embedded.line.js": "javascript", 52 | "source.sql": "sql", 53 | "meta.embedded.block.sql": "sql", 54 | "meta.tag.cfml": "cfml", 55 | "constant.character.escape.hash.cfml": "cfml", 56 | "punctuation.definition.template-expression.begin.cfml": "cfml", 57 | "meta.embedded.line.cfml": "cfml", 58 | "meta.embedded.block.cfml": "cfml", 59 | "punctuation.definition.template-expression.end.cfml": "cfml" 60 | } 61 | } 62 | ], 63 | "configuration": { 64 | "type": "object", 65 | "title": "cflsp", 66 | "properties": { 67 | "cflsp.x_parseTypes": { 68 | "scope": "resource", 69 | "type": "boolean", 70 | "default": false, 71 | "description": "experimental: parse typedefs and types in comments; use `@!type `, `@!typedef = ` and `@!interface { }`" 72 | }, 73 | "cflsp.x_checkReturnTypes": { 74 | "scope": "resource", 75 | "type": "boolean", 76 | "default": false, 77 | "description": "experimental: check return types of functions against function signatures" 78 | }, 79 | "cflsp.x_checkFlowTypes": { 80 | "scope": "resource", 81 | "type": "boolean", 82 | "default": false, 83 | "description": "experimental: use flow analysis to aid type checking" 84 | }, 85 | "cflsp.x_genericFunctionInference": { 86 | "scope": "resource", 87 | "type": "boolean", 88 | "default": false, 89 | "description": "experimental: run inference on generic function signatures (intended use is library functions like Array.map and friends)" 90 | }, 91 | "cflsp.engineVersion": { 92 | "scope": "resource", 93 | "type": "string", 94 | "description": "Which ColdFusion language version to use", 95 | "enum": [ 96 | "acf.2018", 97 | "acf.2021", 98 | "lucee.5" 99 | ], 100 | "default": "lucee.5", 101 | "enumDescriptions": [ 102 | "Adobe 2018", 103 | "Adobe 2021", 104 | "Lucee 5" 105 | ], 106 | "items": ["Adobe/2018", "Adobe/2021", "Lucee/5"] 107 | }, 108 | "cflsp.wireboxResolution": { 109 | "scope": "resource", 110 | "type": "boolean", 111 | "description": "Enable experimental resolution of CFCs via Wirebox's `getInstance`.", 112 | "default": false 113 | }, 114 | "cflsp.cfConfigProjectRelativePath": { 115 | "scope": "resource", 116 | "type": "string", 117 | "description": "Project-relative path to the cfls config file.", 118 | "default": "cfconfig.json" 119 | } 120 | } 121 | } 122 | }, 123 | "scripts": { 124 | }, 125 | "devDependencies": { 126 | "@types/mocha": "^8.0.3", 127 | "@types/node": "^12.12.0", 128 | "@typescript-eslint/parser": "^2.3.0", 129 | "eslint": "^6.4.0", 130 | "mocha": "^8.1.1", 131 | "typescript": "^4.2.2" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/compiler/engines.ts: -------------------------------------------------------------------------------- 1 | import { exhaustiveCaseGuard } from "./utils"; 2 | 3 | export const enum Engine { Adobe = 1, Lucee = 2 }; 4 | 5 | export class Semver { 6 | major: number; 7 | minor: number; 8 | patch: number; 9 | 10 | constructor(major: number, minor: number, patch: number) { 11 | this.major = major; 12 | this.minor = minor; 13 | this.patch = patch; 14 | } 15 | 16 | private compareInt(rhsMajor: number, rhsMinor: number, rhsPatch: number) : -1 | 0 | 1 { 17 | const compare = [ 18 | intCompare(this.major, rhsMajor), 19 | intCompare(this.minor, rhsMinor), 20 | intCompare(this.patch, rhsPatch), 21 | ]; 22 | 23 | for (const v of compare) { 24 | switch (v) { 25 | case -1: return -1; 26 | case 0: continue; 27 | case 1: return 1; 28 | default: exhaustiveCaseGuard(v); 29 | } 30 | } 31 | 32 | return 0; 33 | 34 | function intCompare(l: number, r: number) : -1 | 0 | 1 { 35 | return l < r ? -1 : l === r ? 0 : 1; 36 | } 37 | } 38 | 39 | public compare(rhs: Semver) : -1 | 0 | 1; 40 | public compare(rhsMajor: number, rhsMinor: number, rhsPatch: number) : -1 | 0 | 1; 41 | public compare(rhsMajor: Semver | number, rhsMinor?: number, rhsPatch?: number) : -1 | 0 | 1 { 42 | if (typeof rhsMajor === "number") { 43 | return this.compareInt(rhsMajor, rhsMinor!, rhsPatch!); 44 | } 45 | else { 46 | const rhs = rhsMajor; 47 | return this.compareInt(rhs.major, rhs.minor, rhs.patch); 48 | } 49 | } 50 | 51 | eq(rhsMajor: number, rhsMinor?: number, rhsPatch?: number) { 52 | return this.compareInt(rhsMajor, rhsMinor as number, rhsPatch as number) === 0; 53 | } 54 | 55 | lt(rhsMajor: number, rhsMinor: number, rhsPatch: number) { 56 | return this.compareInt(rhsMajor, rhsMinor, rhsPatch) < 0; 57 | } 58 | 59 | lte(rhsMajor: number, rhsMinor: number, rhsPatch: number) { 60 | return this.compareInt(rhsMajor, rhsMinor, rhsPatch) <= 0; 61 | } 62 | 63 | gt(rhsMajor: number, rhsMinor: number, rhsPatch: number) { 64 | return this.compareInt(rhsMajor, rhsMinor, rhsPatch) > 0; 65 | } 66 | 67 | gte(rhsMajor: number, rhsMinor: number, rhsPatch: number) { 68 | return this.compareInt(rhsMajor, rhsMinor, rhsPatch) >= 0; 69 | } 70 | } 71 | 72 | export interface EngineVersion { 73 | engine: Engine, 74 | semver: Semver, 75 | uiString: string, 76 | } 77 | 78 | function LiteralRecord() { 79 | return >(v: U) : {[k in keyof U]: T} => v; 80 | }; 81 | 82 | export const EngineVersions = LiteralRecord()({ 83 | "acf.2018": { 84 | engine: Engine.Adobe, 85 | semver: new Semver(2018, 0, 0), 86 | uiString: "Adobe/2018" 87 | }, 88 | "acf.2021": { 89 | engine: Engine.Adobe, 90 | semver: new Semver(2021, 0, 0), 91 | uiString: "Adobe/2021", 92 | }, 93 | "lucee.5": { 94 | engine: Engine.Lucee, 95 | semver: new Semver(5, 0, 0), 96 | uiString: "Lucee/5", 97 | } 98 | } as const) 99 | 100 | export const supports = { 101 | /** 102 | * something like: 103 | * " foo ".trim(); 104 | * vs. 105 | * " foo "["trim"](); 106 | */ 107 | bracketAccessIntoStringLiteral(ev: EngineVersion) { 108 | return ev.engine === Engine.Lucee; 109 | }, 110 | trailingStructLiteralComma(ev: EngineVersion) { 111 | return ev.engine === Engine.Lucee; 112 | }, 113 | trailingArrayLiteralComma(ev: EngineVersion) { 114 | return ev.engine === Engine.Lucee; 115 | }, 116 | structLiteralSpread(ev: EngineVersion) { 117 | return ev.engine === Engine.Adobe && ev.semver.gte(2021, 0, 0); 118 | }, 119 | structLiteralShorthand(ev: EngineVersion) { 120 | return ev.engine === Engine.Adobe && ev.semver.gte(2021, 0, 0); 121 | }, 122 | noParenSingleArgArrowFunction(ev: EngineVersion) { 123 | return ev.engine === Engine.Adobe; 124 | } 125 | } as const; -------------------------------------------------------------------------------- /src/services/cflsTypes.ts: -------------------------------------------------------------------------------- 1 | import { EngineVersion, EngineVersions } from "../compiler/engines"; 2 | import type { AbsPath, SafeOmit } from "../compiler/utils"; 3 | 4 | export type CflsRequest = 5 | | DiagnosticsRequest 6 | | CompletionsRequest 7 | | DefinitionLocationsRequest 8 | | InitRequest 9 | | ResetRequest 10 | 11 | export const enum CflsRequestType { 12 | diagnostics, 13 | completions, 14 | definitionLocations, 15 | init, 16 | reset 17 | } 18 | 19 | interface CflsRequestBase { 20 | type: CflsRequestType, 21 | id: number, 22 | } 23 | 24 | // EngineVersion isn't serializeable, we need to transport just the engine name 25 | export type SerializableCflsConfig = SafeOmit & {engineVersion: keyof typeof EngineVersions}; 26 | export interface InitArgs { 27 | config: SerializableCflsConfig, 28 | workspaceRoots: AbsPath[], 29 | cancellationTokenId: string, 30 | } 31 | 32 | export interface InitRequest extends CflsRequestBase { 33 | type: CflsRequestType.init, 34 | initArgs: InitArgs, 35 | } 36 | 37 | export interface DiagnosticsRequest extends CflsRequestBase { 38 | type: CflsRequestType.diagnostics, 39 | fsPath: string, 40 | freshText: string, 41 | sourceRange: null | readonly [fromInclusive: number, toExclusive: number] 42 | } 43 | 44 | export interface CompletionsRequest extends CflsRequestBase { 45 | type: CflsRequestType.completions, 46 | fsPath: string, 47 | targetIndex: number, 48 | triggerCharacter: string | null 49 | } 50 | 51 | export interface DefinitionLocationsRequest extends CflsRequestBase { 52 | type: CflsRequestType.definitionLocations, 53 | fsPath: string, 54 | targetIndex: number 55 | } 56 | export interface ResetRequest extends CflsRequestBase { 57 | type: CflsRequestType.reset 58 | config: SerializableCflsConfig 59 | } 60 | 61 | export const enum CflsResponseType { 62 | initialized, 63 | definitionLocations, 64 | diagnostics, 65 | completions, 66 | cancelled, 67 | } 68 | 69 | export type CflsResponse = 70 | | InitializedResponse 71 | | DiagnosticsResponse 72 | | CompletionsResponse 73 | | DefinitionLocationsResponse 74 | | CancelledResponse 75 | 76 | interface CflsResponseBase { 77 | type: CflsResponseType, 78 | id: number, 79 | } 80 | 81 | interface InitializedResponse extends CflsResponseBase { 82 | type: CflsResponseType.initialized, 83 | // no other info, just "yes we are initialized" 84 | } 85 | interface DiagnosticsResponse extends CflsResponseBase { 86 | type: CflsResponseType.diagnostics, 87 | fsPath: AbsPath, 88 | diagnostics: unknown[] 89 | } 90 | 91 | interface CompletionsResponse extends CflsResponseBase { 92 | type: CflsResponseType.completions, 93 | fsPath: AbsPath, 94 | completionItems: unknown[] 95 | } 96 | 97 | interface DefinitionLocationsResponse extends CflsResponseBase { 98 | type: CflsResponseType.definitionLocations, 99 | locations: unknown[] 100 | } 101 | interface CancelledResponse extends CflsResponseBase { 102 | type: CflsResponseType.cancelled 103 | } 104 | 105 | export interface CflsConfig { 106 | engineLibAbsPath: string | null 107 | x_parseTypes: boolean, 108 | engineVersion: EngineVersion, 109 | cfConfigProjectRelativePath: string | null, 110 | wireboxResolution: boolean, 111 | x_checkReturnTypes: boolean, 112 | x_checkFlowTypes: boolean, 113 | x_genericFunctionInference: boolean, 114 | } 115 | 116 | export function CflsConfig() : CflsConfig { 117 | return { 118 | engineLibAbsPath: null, 119 | x_parseTypes: false, 120 | engineVersion: EngineVersions["lucee.5"], 121 | cfConfigProjectRelativePath: null, 122 | wireboxResolution: false, 123 | x_checkReturnTypes: false, 124 | x_checkFlowTypes: false, 125 | x_genericFunctionInference: false, 126 | } 127 | } 128 | 129 | export function CflsInitArgs(): InitArgs["config"] { 130 | return { 131 | engineLibAbsPath: null, 132 | x_parseTypes: false, 133 | engineVersion: "lucee.5", 134 | cfConfigProjectRelativePath: null, 135 | wireboxResolution: false, 136 | x_checkReturnTypes: false, 137 | x_checkFlowTypes: false, 138 | x_genericFunctionInference: false, 139 | } 140 | } -------------------------------------------------------------------------------- /src/compiler/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "cancellationToken.ts", 4 | "engines.ts", 5 | "scanner.ts", 6 | "node.ts", 7 | "types.ts", 8 | "parser.ts", 9 | "binder.ts", 10 | "checker.ts", 11 | "utils.ts", 12 | "project.ts", 13 | "index.ts", 14 | ], 15 | "compilerOptions": { 16 | "noEmitOnError": false, 17 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 18 | 19 | /* Basic Options */ 20 | // "incremental": true, /* Enable incremental compilation */ 21 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 22 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 23 | "lib": ["es2020"], /* Specify library files to be included in the compilation. */ 24 | // "allowJs": true, /* Allow javascript files to be compiled. */ 25 | // "checkJs": true, /* Report errors in .js files. */ 26 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 27 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 28 | "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 29 | "sourceMap": true, /* Generates corresponding '.map' file. */ 30 | //"outFile": "./out/cfc.js", /* Concatenate and emit output to single file. */ 31 | "outDir": "../../out/compiler", /* Redirect output structure to the directory. */ 32 | //"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 33 | "composite": true, /* Enable project compilation */ 34 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 35 | // "removeComments": true, /* Do not emit comments to output. */ 36 | // "noEmit": true, /* Do not emit outputs. */ 37 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 38 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 39 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 40 | 41 | /* Strict Type-Checking Options */ 42 | "strict": true, /* Enable all strict type-checking options. */ 43 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 44 | "strictNullChecks": true, /* Enable strict null checks. */ 45 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 46 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 47 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 48 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 49 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 50 | "exactOptionalPropertyTypes": true, 51 | 52 | /* Additional Checks */ 53 | "noUnusedLocals": true, /* Report errors on unused locals. */ 54 | "noUnusedParameters": true, /* Report errors on unused parameters. */ 55 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 56 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 57 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 58 | 59 | /* Module Resolution Options */ 60 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 61 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 62 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 63 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 64 | // "typeRoots": [], /* List of folders to include type definitions from. */ 65 | // "types": [], /* Type declaration files to be included in compilation. */ 66 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 67 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 68 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 69 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 70 | 71 | /* Source Map Options */ 72 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 73 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 74 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 75 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 76 | 77 | /* Experimental Options */ 78 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 79 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 80 | 81 | /* Advanced Options */ 82 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 83 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/scratch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./scratch.ts"], 3 | "compilerOptions": { 4 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 5 | 6 | /* Basic Options */ 7 | // "incremental": true, /* Enable incremental compilation */ 8 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 9 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 10 | "lib": ["es2020"], /* Specify library files to be included in the compilation. */ 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "../../out/scratch/", /* Redirect output structure to the directory. */ 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | "composite": true, /* Enable project compilation */ 21 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 22 | // "removeComments": true, /* Do not emit comments to output. */ 23 | // "noEmit": true, /* Do not emit outputs. */ 24 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 25 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 26 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 27 | 28 | /* Strict Type-Checking Options */ 29 | "strict": true, /* Enable all strict type-checking options. */ 30 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 31 | // "strictNullChecks": true, /* Enable strict null checks. */ 32 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 33 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 34 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 35 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 36 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 37 | 38 | /* Additional Checks */ 39 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 40 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 41 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 42 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 43 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | "paths": { 49 | "compiler": ["../compiler"] 50 | }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 51 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 52 | // "typeRoots": [], /* List of folders to include type definitions from. */ 53 | // "types": [], /* Type declaration files to be included in compilation. */ 54 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 55 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 56 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 57 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 58 | 59 | /* Source Map Options */ 60 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 61 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 62 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 63 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 64 | 65 | /* Experimental Options */ 66 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 67 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 68 | 69 | /* Advanced Options */ 70 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 71 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 72 | }, 73 | "references": [ 74 | {"path": "../compiler/"}, 75 | {"path": "../services/"} 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | // 2 | // there is an issue with dependency resolution at runtime with ts-node, 3 | // so make sure to rebuild the compiler before running 4 | // we dynamically link against the build artifacts 5 | // there should be a `tsc --build` command in the root package.json "scripts.test" property 6 | // 7 | { 8 | "compilerOptions": { 9 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 10 | 11 | /* Basic Options */ 12 | // "incremental": true, /* Enable incremental compilation */ 13 | "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 14 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 15 | "lib": ["es2020"], /* Specify library files to be included in the compilation. */ 16 | // "allowJs": true, /* Allow javascript files to be compiled. */ 17 | // "checkJs": true, /* Report errors in .js files. */ 18 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 19 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 20 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 21 | "sourceMap": true, /* Generates corresponding '.map' file. */ 22 | // "outFile": "./", /* Concatenate and emit output to single file. */ 23 | // "outDir": "./", /* Redirect output structure to the directory. */ 24 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 25 | // "composite": true, /* Enable project compilation */ 26 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 27 | // "removeComments": true, /* Do not emit comments to output. */ 28 | // "noEmit": true, /* Do not emit outputs. */ 29 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 30 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 31 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 32 | 33 | /* Strict Type-Checking Options */ 34 | "strict": true, /* Enable all strict type-checking options. */ 35 | "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 36 | "strictNullChecks": true, /* Enable strict null checks. */ 37 | "strictFunctionTypes": true, /* Enable strict checking of function types. */ 38 | "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 39 | "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 40 | "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 41 | "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 42 | 43 | /* Additional Checks */ 44 | //"noUnusedLocals": true, /* Report errors on unused locals. */ 45 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 46 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 47 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 48 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 49 | 50 | /* Module Resolution Options */ 51 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | //"baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 53 | //"paths": { 54 | //"compiler": ["../src/compiler"] 55 | //}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 56 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 57 | // "typeRoots": [], /* List of folders to include type definitions from. */ 58 | // "types": [], /* Type declaration files to be included in compilation. */ 59 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 60 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 61 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 62 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 63 | 64 | /* Source Map Options */ 65 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 66 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 67 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 68 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 69 | 70 | /* Experimental Options */ 71 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 72 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 73 | 74 | /* Advanced Options */ 75 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 76 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 77 | "composite": true 78 | }, 79 | "references": [ 80 | // having trouble getting `npm test` to compile project references, so tests will reference the build artifacts 81 | // this is here in the hopes that eventually we can just reference the ts files directly 82 | {"path": "../src/compiler"}, 83 | {"path": "../src/services"} 84 | ] 85 | } 86 | -------------------------------------------------------------------------------- /src/scratch/scratch.ts: -------------------------------------------------------------------------------- 1 | // quick scratch debug; 2 | // throw some text into the scanner, 3 | // set the parser to either CFM/CFC mode, 4 | // rebuild and then run the debugger 5 | import { Scanner, Parser, Binder, NilDCfm, NilCfc, NilCfm, SourceFile } from "../compiler"; 6 | import { CfFileType } from "../compiler/scanner"; 7 | import { binarySearch, cfmOrCfc, findNodeInFlatSourceMap, flattenTree, isExpressionContext, recursiveGetFiles } from "../compiler/utils"; 8 | import { Checker } from "../compiler/checker"; 9 | import { DebugFileSystem, FileSystem, Project } from "../compiler/project"; 10 | import { EngineVersions } from "../compiler/engines"; 11 | import { getCompletions } from "../services/completions"; 12 | 13 | import * as fs from "fs"; 14 | import * as path from "path"; 15 | 16 | function projectFiddle() { 17 | const debugfs = DebugFileSystem( 18 | { 19 | "/": { 20 | "cfconfig.json": ` 21 | { 22 | "cf": { 23 | "foo": "foo/bar/baz" 24 | }, 25 | "wirebox": { 26 | "someBinding": "foo.someCfc" 27 | } 28 | } 29 | `, 30 | "lib.d.cfm": ` 31 | @!interface Array { PLACEHOLDER: any } 32 | `, 33 | //"realLib.d.cfm": fs.readFileSync("C:\\Users\\anon\\dev\\cfc\\src\\lang-server\\server\\src\\runtimelib\\lib.cf2018.d.cfm").toString(), 34 | "someFolder": { 35 | "someFile.cfc": ` 36 | component { 37 | function foo() { 38 | 39 | } 40 | } 41 | ` 42 | }, 43 | "foo": { 44 | "bar": { 45 | "baz": { 46 | "someCfc.cfc": ` 47 | component { 48 | function fromSomeResolvedWireboxComponent() { 49 | return {helloWorld: 42}; 50 | } 51 | } 52 | `, 53 | } 54 | } 55 | }, 56 | } 57 | } 58 | ); 59 | 60 | //let x = debugfs.readFileSync("/Child.cfc").toString().slice(102,105) 61 | 62 | const project = Project("/", debugfs, { 63 | debug: true, 64 | parseTypes: true, 65 | engineVersion: EngineVersions["lucee.5"], 66 | withWireboxResolution: true, 67 | cfConfigProjectRelativePath: "/Wirebox.cfc", 68 | checkReturnTypes: true, 69 | checkFlowTypes: true, 70 | genericFunctionInference: true, 71 | cancellationToken: { 72 | cancellationRequested: () => false, 73 | throwIfCancellationRequested: () => void 0 74 | } 75 | }); 76 | //const project = Project([path.resolve(".")], FileSystem(), {debug: true, parseTypes: true, language: LanguageVersion.lucee5}); 77 | //const target = path.join(path.resolve("./test/"), "mxunit/framework/javaloader/JavaProxy.cfc"); 78 | 79 | //project.addEngineLib("/lib.d.cfm"); 80 | // project.addEngineLib("/realLib.d.cfm"); 81 | project.addFile("/someFolder/someFile.cfc"); 82 | //project.addFile("C:\\Users\\anon\\dev\\cb\\testbox\\tests\\resources\\coldbox\\system\\EventHandler.cfc"); 83 | const diagnostics = project.getDiagnostics("/someFolder/someFile.cfc"); 84 | 85 | const x = project.getParsedSourceFile("/someFolder/someFile.cfc"); 86 | console.log(x?.containedScope.variables?.get("xname")) 87 | 88 | //const x = project.getInterestingNodeToLeftOfCursor("/someFile.cfc", 378); 89 | //const completions = getCompletions(project, "/someFile.cfc", 381, null); 90 | //console.log(completions); 91 | for (const diagnostic of diagnostics) { 92 | console.log(diagnostic); 93 | } 94 | } 95 | 96 | function bench() { 97 | function recursiveGetFiles(root: string, pattern: RegExp) : string [] { 98 | const result : string[] = []; 99 | const fds = fs.readdirSync(root, {withFileTypes: true}); 100 | for (const fd of fds) { 101 | if (fd.isDirectory()) result.push(...recursiveGetFiles(path.resolve(root, fd.name), pattern)); 102 | else if (pattern.test(fd.name)) { 103 | const fspath = path.resolve(root, fd.name); 104 | result.push(fspath); 105 | } 106 | } 107 | return result; 108 | } 109 | 110 | const files = recursiveGetFiles(process.env.XPATH as string, /\.(cfm|cfc)$/i); 111 | 112 | const parser = Parser({ 113 | debug: false, 114 | parseTypes: true, 115 | engineVersion: EngineVersions["lucee.5"], 116 | withWireboxResolution: true, 117 | cfConfigProjectRelativePath: "cfconfig.json", 118 | checkReturnTypes: true, 119 | checkFlowTypes: true, 120 | genericFunctionInference: true, 121 | cancellationToken: { 122 | cancellationRequested: () => false, 123 | throwIfCancellationRequested: () => void 0 124 | } 125 | }); 126 | 127 | const times : bigint[] = [] 128 | let lines = 0; 129 | 130 | for (const file of files) { 131 | const sourceBuffer = fs.readFileSync(file) 132 | 133 | const re = /\r|\n|\r\n/g; 134 | let linesThis = 0; 135 | const asString = sourceBuffer.toString(); 136 | while (re.exec(asString)) linesThis++; 137 | 138 | 139 | const sourceFile = SourceFile(file, cfmOrCfc(file)!, sourceBuffer); 140 | const start = process.hrtime.bigint(); 141 | parser.parse(sourceFile); 142 | const end = process.hrtime.bigint(); 143 | 144 | if (linesThis > 1000) { 145 | times.push(end - start); 146 | lines += linesThis; 147 | console.log(file, linesThis + " lines in ", ((end-start) / BigInt(1e6)) + "ms") 148 | } 149 | 150 | 151 | } 152 | 153 | let timeSum_ns = BigInt(0); 154 | for (const time of times) timeSum_ns += time; 155 | const avgTime_ns = timeSum_ns / BigInt(times.length); 156 | const avgTime_ms = avgTime_ns / BigInt(1e6) 157 | 158 | console.log("Total time: " + (timeSum_ns / BigInt(1e6)) + "ms") 159 | console.log("Avg parsetime: " + avgTime_ms + "ms"); 160 | console.log("Total lines: " + lines); 161 | console.log("Lines/sec: " + (BigInt(lines) / (timeSum_ns / BigInt(1e6))) * BigInt(1000)); 162 | } 163 | 164 | //bench() 165 | projectFiddle(); 166 | 167 | 168 | /*function xfiddle() { 169 | const files = recursiveGetFiles("c:/users/anon/dev/coldbox/", /\.cfc$/i); 170 | const project = Project(["c:\\users\\anon\\dev\\coldbox\\"], FileSystem(), {debug: false, parseTypes: false, language: LanguageVersion.acf2018}); 171 | project.addEngineLib("c:\\Users\\anon\\dev\\cfc\\cflsp-vscode\\out\\lib.cf2018.d.cfm") 172 | for (const file of files) { 173 | console.log(file); 174 | project.addFile(file); 175 | } 176 | console.log("done"); 177 | } 178 | 179 | xfiddle();*/ 180 | 181 | -------------------------------------------------------------------------------- /src/lang-server/client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lsp-sample-client", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/vscode": { 8 | "version": "1.52.0", 9 | "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.52.0.tgz", 10 | "integrity": "sha512-Kt3bvWzAvvF/WH9YEcrCICDp0Z7aHhJGhLJ1BxeyNP6yRjonWqWnAIh35/pXAjswAnWOABrYlF7SwXR9+1nnLA==", 11 | "dev": true 12 | }, 13 | "agent-base": { 14 | "version": "4.3.0", 15 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", 16 | "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", 17 | "dev": true, 18 | "requires": { 19 | "es6-promisify": "^5.0.0" 20 | } 21 | }, 22 | "balanced-match": { 23 | "version": "1.0.0", 24 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 25 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 26 | }, 27 | "brace-expansion": { 28 | "version": "1.1.11", 29 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 30 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 31 | "requires": { 32 | "balanced-match": "^1.0.0", 33 | "concat-map": "0.0.1" 34 | } 35 | }, 36 | "concat-map": { 37 | "version": "0.0.1", 38 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 39 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 40 | }, 41 | "debug": { 42 | "version": "3.1.0", 43 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 44 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 45 | "dev": true, 46 | "requires": { 47 | "ms": "2.0.0" 48 | } 49 | }, 50 | "es6-promise": { 51 | "version": "4.2.8", 52 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 53 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", 54 | "dev": true 55 | }, 56 | "es6-promisify": { 57 | "version": "5.0.0", 58 | "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", 59 | "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", 60 | "dev": true, 61 | "requires": { 62 | "es6-promise": "^4.0.3" 63 | } 64 | }, 65 | "fs.realpath": { 66 | "version": "1.0.0", 67 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 68 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 69 | "dev": true 70 | }, 71 | "glob": { 72 | "version": "7.1.6", 73 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 74 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 75 | "dev": true, 76 | "requires": { 77 | "fs.realpath": "^1.0.0", 78 | "inflight": "^1.0.4", 79 | "inherits": "2", 80 | "minimatch": "^3.0.4", 81 | "once": "^1.3.0", 82 | "path-is-absolute": "^1.0.0" 83 | } 84 | }, 85 | "http-proxy-agent": { 86 | "version": "2.1.0", 87 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", 88 | "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", 89 | "dev": true, 90 | "requires": { 91 | "agent-base": "4", 92 | "debug": "3.1.0" 93 | } 94 | }, 95 | "https-proxy-agent": { 96 | "version": "2.2.4", 97 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", 98 | "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", 99 | "dev": true, 100 | "requires": { 101 | "agent-base": "^4.3.0", 102 | "debug": "^3.1.0" 103 | } 104 | }, 105 | "inflight": { 106 | "version": "1.0.6", 107 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 108 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 109 | "dev": true, 110 | "requires": { 111 | "once": "^1.3.0", 112 | "wrappy": "1" 113 | } 114 | }, 115 | "inherits": { 116 | "version": "2.0.4", 117 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 118 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 119 | "dev": true 120 | }, 121 | "lru-cache": { 122 | "version": "6.0.0", 123 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 124 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 125 | "requires": { 126 | "yallist": "^4.0.0" 127 | } 128 | }, 129 | "minimatch": { 130 | "version": "3.0.4", 131 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 132 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 133 | "requires": { 134 | "brace-expansion": "^1.1.7" 135 | } 136 | }, 137 | "ms": { 138 | "version": "2.0.0", 139 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 140 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 141 | "dev": true 142 | }, 143 | "once": { 144 | "version": "1.4.0", 145 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 146 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 147 | "dev": true, 148 | "requires": { 149 | "wrappy": "1" 150 | } 151 | }, 152 | "path-is-absolute": { 153 | "version": "1.0.1", 154 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 155 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 156 | "dev": true 157 | }, 158 | "rimraf": { 159 | "version": "2.7.1", 160 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", 161 | "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", 162 | "dev": true, 163 | "requires": { 164 | "glob": "^7.1.3" 165 | } 166 | }, 167 | "semver": { 168 | "version": "7.3.4", 169 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", 170 | "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", 171 | "requires": { 172 | "lru-cache": "^6.0.0" 173 | } 174 | }, 175 | "vscode-jsonrpc": { 176 | "version": "6.0.0", 177 | "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", 178 | "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==" 179 | }, 180 | "vscode-languageclient": { 181 | "version": "7.0.0", 182 | "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", 183 | "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", 184 | "requires": { 185 | "minimatch": "^3.0.4", 186 | "semver": "^7.3.4", 187 | "vscode-languageserver-protocol": "3.16.0" 188 | } 189 | }, 190 | "vscode-languageserver-protocol": { 191 | "version": "3.16.0", 192 | "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", 193 | "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", 194 | "requires": { 195 | "vscode-jsonrpc": "6.0.0", 196 | "vscode-languageserver-types": "3.16.0" 197 | } 198 | }, 199 | "vscode-languageserver-types": { 200 | "version": "3.16.0", 201 | "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", 202 | "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==" 203 | }, 204 | "vscode-test": { 205 | "version": "1.3.0", 206 | "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.3.0.tgz", 207 | "integrity": "sha512-LddukcBiSU2FVTDr3c1D8lwkiOvwlJdDL2hqVbn6gIz+rpTqUCkMZSKYm94Y1v0WXlHSDQBsXyY+tchWQgGVsw==", 208 | "dev": true, 209 | "requires": { 210 | "http-proxy-agent": "^2.1.0", 211 | "https-proxy-agent": "^2.2.4", 212 | "rimraf": "^2.6.3" 213 | } 214 | }, 215 | "vscode-uri": { 216 | "version": "3.0.2", 217 | "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.2.tgz", 218 | "integrity": "sha512-jkjy6pjU1fxUvI51P+gCsxg1u2n8LSt0W6KrCNQceaziKzff74GoWmjVG46KieVzybO1sttPQmYfrwSHey7GUA==" 219 | }, 220 | "wrappy": { 221 | "version": "1.0.2", 222 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 223 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 224 | "dev": true 225 | }, 226 | "yallist": { 227 | "version": "4.0.0", 228 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 229 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 230 | } 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "../../out/build/", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | 68 | /* Interop Constraints */ 69 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 70 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 71 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 72 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 73 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 74 | 75 | /* Type Checking */ 76 | "strict": true, /* Enable all strict type-checking options. */ 77 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 78 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 79 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 80 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 81 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 82 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 83 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 84 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 85 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 86 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 87 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 88 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 89 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 90 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 91 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 92 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 93 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 94 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 95 | 96 | /* Completeness */ 97 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 98 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/services/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": ["es2020"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "../", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": { 32 | // "compiler": ["./compiler"] 33 | // }, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "resolveJsonModule": true, /* Enable importing .json files */ 39 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 40 | 41 | /* JavaScript Support */ 42 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 43 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 44 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 45 | 46 | /* Emit */ 47 | //"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 48 | //"declarationMap": true, /* Create sourcemaps for d.ts files. */ 49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 50 | "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 51 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 52 | "outDir": "../../out/services/", /* Specify an output folder for all emitted files. */ 53 | // "removeComments": true, /* Disable emitting comments. */ 54 | // "noEmit": true, /* Disable emitting file from a compilation. */ 55 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 56 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 57 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 58 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 63 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 64 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 67 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 69 | 70 | /* Interop Constraints */ 71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 72 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 73 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 74 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 75 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 76 | 77 | /* Type Checking */ 78 | "strict": true, /* Enable all strict type-checking options. */ 79 | "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 80 | "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 81 | "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 82 | "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 83 | "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 84 | "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 85 | "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 86 | "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 87 | "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 88 | "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 89 | "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 90 | "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 91 | "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 92 | "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 93 | "noImplicitOverride": true, /* Add `undefined` to a type when accessed using an index. */ 94 | "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 97 | 98 | /* Completeness */ 99 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 100 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 101 | }, 102 | "references": [ 103 | {"path": "../compiler"} 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /src/services/languageService.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * the languageService is the interop between something that can speak LSP and our own language tooling 3 | * we don't understand LSP here, but we provide an interface for something that does to reach out to us for diagnostics or completions or etc. 4 | * we fork the actual language tool such that long-running requests don't block the entire server, and so that such long-requests can be cancelled 5 | * 6 | * cancellation is acheived with a cancellation token, which is at its core just a particular file on disk (a named pipe) whose existence 7 | * can be tested for and, when it exists, means "do cancel" 8 | */ 9 | 10 | import * as child_process from "child_process"; 11 | import * as path from "path"; 12 | import { CancellationToken } from "../compiler/cancellationToken"; 13 | import { CflsResponse, CflsResponseType, CflsRequest, CflsRequestType, CflsConfig, InitArgs } from "./cflsTypes"; 14 | import { ClientAdapter } from "./clientAdapter"; 15 | import type { AbsPath } from "../compiler/utils"; 16 | import type { IREPLACED_AT_BUILD } from "./buildShim"; 17 | import { SourceRange } from "../compiler/scanner"; 18 | 19 | declare const REPLACED_AT_BUILD : IREPLACED_AT_BUILD; 20 | 21 | interface EventHandlers { 22 | diagnostics: (fsPath: string, diagnostics: unknown[]) => void 23 | } 24 | 25 | export function LanguageService>() { 26 | const forkInfo = { 27 | languageToolFilePath: path.join(__dirname, REPLACED_AT_BUILD.runtimeLanguageToolPath), 28 | forkArgs: REPLACED_AT_BUILD.debug 29 | ? {execArgv: ["--nolazy", "--inspect=6012", /*"--inspect-brk"*/]} 30 | : {} 31 | } as const; 32 | 33 | let server! : child_process.ChildProcess; 34 | let config! : InitArgs["config"]; // we keep a copy to diff reset requests against; this is probably unnecessary, a holdover from when we were accidentally registering to receive all client configuration changes 35 | const cancellationToken = CancellationToken(); 36 | const handlerMappings : Partial = {}; 37 | 38 | interface TaskDef { 39 | type: CflsRequestType, 40 | task: () => void, 41 | timeout_ms?: number, 42 | onSuccess?: (payload?: any) => void 43 | onNoResponse?: () => void 44 | } 45 | 46 | let taskQueue : TaskDef[] = []; 47 | let currentTask: TaskDef | null = null; 48 | let requestTimeoutId : NodeJS.Timeout | null = null; 49 | 50 | const messageId = (() => { 51 | let id = 0; 52 | return { 53 | bump: () => { 54 | id = id === Number.MAX_SAFE_INTEGER 55 | ? 0 56 | : id + 1; 57 | return id; 58 | }, 59 | current: () => id 60 | } 61 | })(); 62 | 63 | function fork(freshConfig: InitArgs["config"], workspaceRoots: AbsPath[]) : Promise { 64 | config = freshConfig; 65 | server = child_process.fork(forkInfo.languageToolFilePath, forkInfo.forkArgs); 66 | 67 | server.on("message", (msg: CflsResponse) => { 68 | if (messageId.current() === msg.id) { 69 | clearRequestTimeout(); 70 | switch (msg.type) { 71 | case CflsResponseType.initialized: { 72 | if (currentTask?.onSuccess) currentTask.onSuccess(); 73 | break; 74 | } 75 | case CflsResponseType.diagnostics: { 76 | handlerMappings.diagnostics?.(msg.fsPath, msg.diagnostics); 77 | break; 78 | } 79 | case CflsResponseType.completions: { 80 | if (currentTask?.onSuccess) currentTask.onSuccess(msg.completionItems); 81 | break; 82 | } 83 | case CflsResponseType.definitionLocations: { 84 | if (currentTask?.onSuccess) currentTask.onSuccess(msg.locations); 85 | break; 86 | } 87 | } 88 | } 89 | runNextTask(); 90 | }) 91 | 92 | const {resolve, promise} = explodedPromise(); 93 | const initTask : TaskDef = { 94 | type: CflsRequestType.init, 95 | task: () => send({ 96 | type: CflsRequestType.init, 97 | id: messageId.bump(), 98 | initArgs: { 99 | config, 100 | workspaceRoots, 101 | cancellationTokenId: cancellationToken.getId(), 102 | }, 103 | }), 104 | timeout_ms: 1000 * 60 * 2, // long timeout for init request, to allow for setting up path mappings and etc. 105 | onSuccess: () => { 106 | resolve(void 0); 107 | } 108 | }; 109 | 110 | pushTask(initTask); 111 | 112 | return promise; 113 | } 114 | 115 | function runNextTask() { 116 | clearRequestTimeout(); 117 | const task = taskQueue.shift(); 118 | if (task) { 119 | if (task.type === CflsRequestType.diagnostics) { 120 | taskQueue = taskQueue.filter(task => task.type !== CflsRequestType.diagnostics) 121 | } 122 | currentTask = task; 123 | requestTimeoutId = setTimeout(noResponseAndRunNext, task.timeout_ms ?? 5000); 124 | task.task(); 125 | } 126 | else { 127 | currentTask = null; 128 | } 129 | } 130 | 131 | function noResponseAndRunNext() { 132 | currentTask?.onNoResponse?.(); 133 | runNextTask(); 134 | } 135 | 136 | function clearRequestTimeout() { 137 | if (requestTimeoutId) { 138 | clearTimeout(requestTimeoutId); 139 | requestTimeoutId = null; 140 | } 141 | } 142 | 143 | function on(eventName: K, handler: EventHandlers[K]) { 144 | handlerMappings[eventName] = handler; 145 | } 146 | 147 | function send(msg: CflsRequest) { 148 | server.send(msg); 149 | } 150 | 151 | function emitDiagnostics(fsPath: AbsPath, freshText: string, sourceRange?: SourceRange) { 152 | const task = () => { 153 | const serializedRange = sourceRange ? [sourceRange.fromInclusive, sourceRange.toExclusive] as const : null; 154 | const request : CflsRequest = {type: CflsRequestType.diagnostics, id: messageId.bump(), fsPath, freshText, sourceRange: serializedRange}; 155 | send(request); 156 | } 157 | pushTask({type: CflsRequestType.diagnostics, task}); 158 | } 159 | 160 | function pushTask(taskDef: TaskDef) { 161 | if (currentTask) { 162 | // "diagnostic" task is conceptually overloaded to be responsible for doing all the heavy lifiting of loading a file into the project, 163 | // and parse/bind/check to get a tree, so that other tasks have a good tree to pull info from 164 | // so we treat it specially, and if the current task is diagnostics, we cancel the current diagnostic request and queue up another run 165 | if (taskDef.type === CflsRequestType.diagnostics && currentTask.type === CflsRequestType.diagnostics) { 166 | taskQueue = [{ 167 | type: taskDef.type, 168 | task: () => { 169 | cancellationToken.reset(); 170 | taskDef.task() 171 | } 172 | }, ...taskQueue.filter((task) => task.type !== CflsRequestType.diagnostics)]; // do we need to tell the language server that we possibly dropped some requests? 173 | 174 | cancellationToken.requestCancellation(); 175 | } 176 | else { 177 | taskQueue.push(taskDef); 178 | } 179 | } 180 | else { 181 | // no current tasks; queue it and then immediately run it 182 | taskQueue.push(taskDef); 183 | runNextTask(); 184 | } 185 | } 186 | 187 | function reset(freshConfig: InitArgs["config"]) { 188 | let didChange = false; 189 | for (const key of Object.keys(freshConfig) as (keyof CflsConfig)[]) { 190 | if (freshConfig[key] !== config[key]) { 191 | didChange = true; 192 | break; 193 | } 194 | } 195 | 196 | if (!didChange) { 197 | return; 198 | } 199 | 200 | config = freshConfig; 201 | send({type: CflsRequestType.reset, id: messageId.bump(), config}); 202 | } 203 | 204 | function explodedPromise() { 205 | let resolve!: (value: T) => void; 206 | let reject!: () => void; 207 | const promise = new Promise((ok, fail) => [resolve, reject] = [ok, fail]); 208 | return {resolve, reject, promise}; 209 | } 210 | 211 | function getCompletions(fsPath: AbsPath, targetIndex: number, triggerCharacter: string | null) : Promise[]> { 212 | const {promise, resolve} = explodedPromise[]>(); 213 | 214 | pushTask({ 215 | type: CflsRequestType.completions, 216 | task: () => { 217 | send({ 218 | type: CflsRequestType.completions, 219 | id: messageId.bump(), 220 | fsPath, 221 | targetIndex, 222 | triggerCharacter 223 | }) 224 | }, 225 | onSuccess: (payload: ReturnType[]) => { 226 | resolve(payload); 227 | }, 228 | onNoResponse: () => resolve([]) 229 | }) 230 | 231 | return promise; 232 | } 233 | 234 | function getDefinitionLocations(fsPath: AbsPath, targetIndex: number) { 235 | const {promise, resolve} = explodedPromise[]>(); 236 | 237 | pushTask({ 238 | type: CflsRequestType.definitionLocations, 239 | task: () => { 240 | send({ 241 | type: CflsRequestType.definitionLocations, 242 | id: messageId.bump(), 243 | fsPath, 244 | targetIndex, 245 | }) 246 | }, 247 | onSuccess: (payload: ReturnType[]) => { 248 | resolve(payload); 249 | }, 250 | onNoResponse: () => resolve([]) 251 | }) 252 | 253 | return promise; 254 | } 255 | 256 | return { 257 | reset, 258 | fork, 259 | on, 260 | emitDiagnostics, 261 | getCompletions, 262 | getDefinitionLocations, 263 | } 264 | } 265 | 266 | export type LanguageService> = ReturnType> -------------------------------------------------------------------------------- /src/lang-server/server/src/server.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import { 6 | createConnection, 7 | ProposedFeatures, 8 | InitializeParams, 9 | DidChangeConfigurationNotification, 10 | CompletionItem, 11 | TextDocumentSyncKind, 12 | InitializeResult, 13 | CompletionParams, 14 | WorkspaceFolder, 15 | TextDocumentContentChangeEvent, 16 | } from 'vscode-languageserver/node'; 17 | 18 | import { Location } from "vscode-languageserver-types" 19 | import { 20 | TextDocument 21 | } from 'vscode-languageserver-textdocument'; 22 | 23 | 24 | import { URI } from "vscode-uri"; 25 | 26 | import { LanguageTool } from "../../../services/languageTool" 27 | import { CflsInitArgs, SerializableCflsConfig } from "../../../services/cflsTypes" 28 | import { SourceRange } from '../../../compiler/scanner'; 29 | import { CancellationToken } from '../../../compiler/cancellationToken'; 30 | 31 | import { adapter as resultsAdapter } from "./vscode-direct-adapter" 32 | 33 | const languageTool = LanguageTool(); 34 | const knownDocs = new Map(); 35 | const cancellationToken = CancellationToken(); 36 | 37 | // unwrap is required to make it harder to accidentally use in a position where we wanted a local with a similar name 38 | const initArgs = Object.freeze((() => { 39 | const value = CflsInitArgs(); 40 | return {unwrap: () => value}; 41 | })()); 42 | 43 | // Create a connection for the server, using Node's IPC as a transport. 44 | // Also include all preview / proposed LSP features. 45 | let connection = createConnection(ProposedFeatures.all); 46 | 47 | // Create a simple text document manager. 48 | 49 | let workspaceRoots : WorkspaceFolder[] = []; 50 | 51 | let hasConfigurationCapability: boolean = false; 52 | let hasWorkspaceFolderCapability: boolean = false; 53 | let hasDiagnosticRelatedInformationCapability: boolean = false; 54 | 55 | connection.onInitialize(async (params: InitializeParams) => { 56 | let capabilities = params.capabilities; 57 | 58 | let roots = params.workspaceFolders; 59 | if (roots) { 60 | workspaceRoots = roots; 61 | connection.console.info("cflsp/initialize, workspaces:") 62 | connection.console.info(roots.map(e=>e.uri).join(",")); 63 | } 64 | 65 | // Does the client support the `workspace/configuration` request? 66 | // If not, we fall back using global settings. 67 | hasConfigurationCapability = !!( 68 | capabilities.workspace && !!capabilities.workspace.configuration 69 | ); 70 | hasWorkspaceFolderCapability = !!( 71 | capabilities.workspace && !!capabilities.workspace.workspaceFolders 72 | ); 73 | hasDiagnosticRelatedInformationCapability = !!( 74 | capabilities.textDocument && 75 | capabilities.textDocument.publishDiagnostics && 76 | capabilities.textDocument.publishDiagnostics.relatedInformation 77 | ); 78 | 79 | const result: InitializeResult = { 80 | capabilities: { 81 | textDocumentSync: TextDocumentSyncKind.Incremental, 82 | // Tell the client that this server supports code completion. 83 | completionProvider: {triggerCharacters: ["\"", "'", ".", " ", "("]}, 84 | //signatureHelpProvider: {triggerCharacters: ["("]}, 85 | definitionProvider: true, 86 | //hoverProvider: true, 87 | } 88 | }; 89 | 90 | if (typeof params.initializationOptions?.libAbsPath === "string") { 91 | initArgs.unwrap().engineLibAbsPath = params.initializationOptions?.libAbsPath; 92 | } 93 | 94 | const config = params.initializationOptions.config; 95 | 96 | // 97 | // init with no config, because it's impossible to ask for workspace/configuration from within onInitialize? 98 | // we get `rejected promise not handled within 1 second: Error: Unhandled method workspace/configuration` if we do 99 | // `await connection.connection.workspace.getConfiguration("cflsp")` here, but it's ok in "onInitialized" and elsewhere? 100 | // 101 | const freshInitArgs = mungeConfig(config); 102 | languageTool.init({ 103 | config: freshInitArgs, 104 | cancellationTokenId: cancellationToken.getId(), 105 | workspaceRoots: workspaceRoots.map((root) => URI.parse(root.uri).fsPath) 106 | }); 107 | 108 | return result; 109 | }); 110 | // show where a symbol is defined at 111 | connection.onDefinition(async (params) : Promise => { 112 | const doc = knownDocs.get(params.textDocument.uri); 113 | if (!doc) { 114 | return undefined; 115 | } 116 | 117 | const fsPath = URI.parse(doc.uri).fsPath; 118 | const targetIndex = doc.offsetAt(params.position); 119 | 120 | const maybeDefLocations = languageTool.getDefinitionLocations(fsPath, targetIndex); 121 | if (maybeDefLocations) { 122 | const mapped = maybeDefLocations.map(defLoc => { 123 | const sourceDocUri = URI.parse(defLoc.sourceFile.absPath).toString(); 124 | // 125 | // we might not know about the doc here, because the langauge service opened it off the file system as dependency, but the 126 | // user doesn't have it open, so we don't know about it in the list of "open vscode docs". 127 | // 128 | const sourceDoc = knownDocs.get(sourceDocUri) || TextDocument.create("", "cfml", -1, defLoc.sourceFile.scanner.getSourceText()); 129 | const posMapper = resultsAdapter.cfPositionToVsPosition(sourceDoc); 130 | return resultsAdapter.sourceLocation(posMapper, defLoc); 131 | }); 132 | return mapped; 133 | } 134 | else { 135 | return undefined; 136 | } 137 | }); 138 | 139 | function mungeConfig(config: Record | null) : SerializableCflsConfig { 140 | // engineLibAbsPath doesn't come from config, so we just carry it forward if it exists 141 | return { 142 | engineLibAbsPath: initArgs.unwrap().engineLibAbsPath ?? null, 143 | // the rest of these are supplied via config 144 | x_parseTypes: !!config?.x_parseTypes, 145 | x_genericFunctionInference: !!config?.x_genericFunctionInference, 146 | x_checkReturnTypes: !!config?.x_checkReturnTypes, 147 | x_checkFlowTypes: !!config?.x_checkFlowTypes, 148 | engineVersion: config?.engineVersion ?? "lucee.5", 149 | wireboxResolution: config?.wireboxResolution ?? false, 150 | cfConfigProjectRelativePath: config?.cfConfigProjectRelativePath ?? null, 151 | } 152 | } 153 | 154 | connection.onDidChangeConfiguration(async cflsConfig => { 155 | const freshInitArgs = mungeConfig(cflsConfig.settings.cflsp); 156 | languageTool.reset(freshInitArgs); 157 | }); 158 | 159 | function runDiagonstics(uri: string, textDocument: string, sourceRangeIfIncremental?: {sourceRange: SourceRange, changeSize: number}) { 160 | const doc = knownDocs.get(uri); 161 | if (!doc) { 162 | // shouldn't happen 163 | return; 164 | } 165 | 166 | const fsPath = URI.parse(uri).fsPath; 167 | const diagnostics = languageTool.naiveGetDiagnostics(fsPath, textDocument, sourceRangeIfIncremental); 168 | if (diagnostics) { 169 | const posMapper = resultsAdapter.cfPositionToVsPosition(doc) 170 | connection.sendDiagnostics({ 171 | uri, 172 | diagnostics: diagnostics.diagnostics.map(diagnostic => resultsAdapter.diagnostic(posMapper, diagnostic)) 173 | }); 174 | } 175 | } 176 | 177 | connection.onDidOpenTextDocument(v => { 178 | if (v.textDocument.languageId === 'cfml') { 179 | knownDocs.set(v.textDocument.uri, TextDocument.create(v.textDocument.uri, v.textDocument.languageId, v.textDocument.version, v.textDocument.text)); 180 | try { 181 | runDiagonstics(v.textDocument.uri, v.textDocument.text); 182 | } 183 | catch (err) { 184 | console.error("onDidOpen --", err); 185 | } 186 | } 187 | }) 188 | 189 | /** 190 | * A vscode TextDocumentContentChangeEvent converted into offsets and size info. 191 | * The [from,to) range is the location of the mutation in the **old document**. A range here can be zero length, meaning some edit 192 | * directly on a cursor, like a typical character insertion or deletion, e.g. 193 | * - "a|b" -> "axb" 194 | * - "a|bc" -> "ac" 195 | * 196 | * vvvvvvvvvvvvv ---- toExclusive? or some computed thing as a function of from/to/size? 197 | * The "size delta" is the distance all characters after `fromInclusive` have moved, in the **new document**, e.g. 198 | * - "abc|d" -> "abcxd" sizeDelta=+1 199 | * - "abc|xd" -> "abcd" sizeDelta=-1 200 | * 201 | * ab|cd|ef --> abCDCDef (fromInc=2, toExc=4, sizeDelta=+2) 202 | * ^^ paste CDCD 203 | * 204 | * ab|cd|ef --> abef (fromInc=2, toExc=4, sizeDelta=-2) 205 | * ^^ delete 206 | * 207 | */ 208 | interface MungedTextDocumentContentChangeEvent { 209 | fromInclusive: number, 210 | toExclusive: number, 211 | sizeDelta: number 212 | } 213 | 214 | function contentChangeToXContentChange(doc: TextDocument, incrementalChange: TextDocumentContentChangeEvent) : MungedTextDocumentContentChangeEvent { 215 | if (!TextDocumentContentChangeEvent.isIncremental(incrementalChange)) { 216 | throw "should have been filtered prior to getting here"; 217 | } 218 | 219 | const fromInclusive = doc.offsetAt(incrementalChange.range.start); 220 | const toExclusive = doc.offsetAt(incrementalChange.range.end); 221 | const sizeDelta = incrementalChange.text.length - (toExclusive - fromInclusive) 222 | return {fromInclusive, toExclusive, sizeDelta} 223 | } 224 | 225 | function mergeContentChangeRange(changes: readonly MungedTextDocumentContentChangeEvent[]) : {affectedTextRange: SourceRange, changeSize: number} | undefined { 226 | if (changes.length === 0) { 227 | return undefined; 228 | } 229 | else if (changes.length === 1) { 230 | const change = changes[0]; 231 | return { 232 | affectedTextRange: new SourceRange(change.fromInclusive, change.toExclusive), 233 | changeSize: change.sizeDelta 234 | } 235 | } 236 | else { 237 | const fromInclusive = Math.min(...changes.map(v => v.fromInclusive)); 238 | const toExclusive = Math.max(...changes.map(v => v.toExclusive)); 239 | 240 | // change size is different from "affected range", 241 | // ex. we changed range 100-200, by changeSize=-50, by deleting half of it 242 | const changeSize = (() => { 243 | let c = 0; 244 | changes.forEach(change => { c += change.sizeDelta; }) 245 | return c; 246 | })(); 247 | 248 | return { 249 | affectedTextRange: new SourceRange(fromInclusive, toExclusive), 250 | changeSize 251 | } 252 | } 253 | } 254 | 255 | connection.onDidChangeTextDocument(changes => { 256 | const doc = knownDocs.get(changes.textDocument.uri) 257 | 258 | if (!doc) { 259 | return; 260 | } 261 | 262 | const x = changes 263 | .contentChanges 264 | .filter(TextDocumentContentChangeEvent.isIncremental) 265 | .map(change => contentChangeToXContentChange(doc, change)); 266 | const y = mergeContentChangeRange(x); 267 | 268 | // works in-place on `doc` 269 | TextDocument.update(doc, changes.contentChanges, doc.version + 1); 270 | 271 | try { 272 | runDiagonstics(changes.textDocument.uri, doc.getText(), y ? {sourceRange: y.affectedTextRange, changeSize: y.changeSize} : undefined); 273 | } 274 | catch (err) { 275 | console.error("onDidChange --", err); 276 | } 277 | }) 278 | 279 | connection.onDidCloseTextDocument(v => { 280 | knownDocs.delete(v.textDocument.uri); 281 | connection.sendDiagnostics({ uri: v.textDocument.uri, diagnostics: [] }); 282 | }) 283 | 284 | 285 | connection.onCompletion((completionParams: CompletionParams): CompletionItem[] => { 286 | const doc = knownDocs.get(completionParams.textDocument.uri); 287 | if (!doc) { 288 | return []; 289 | } 290 | 291 | const fsPath = URI.parse(doc.uri).fsPath; 292 | const targetIndex = doc.offsetAt(completionParams.position); 293 | const triggerCharacter = completionParams.context?.triggerCharacter ?? null; 294 | 295 | const items = languageTool.getCompletions(fsPath, targetIndex, triggerCharacter)?.completions 296 | if (items) { 297 | const posMapper = resultsAdapter.cfPositionToVsPosition(doc) 298 | return items.map(item => resultsAdapter.completionItem(posMapper, item)); 299 | } 300 | else { 301 | return []; 302 | } 303 | }); 304 | 305 | connection.listen(); 306 | -------------------------------------------------------------------------------- /src/services/languageTool.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The languageTool accepts requests (our own, limited set of request types, not the LSP request types) and processes 3 | * them, adding/dropping files from a project, checking files, finding completions, etc. 4 | */ 5 | import type { IREPLACED_AT_BUILD } from "./buildShim"; 6 | import { Project, FileSystem } from "../compiler/project" 7 | import { CancellationTokenConsumer } from "../compiler/cancellationToken"; 8 | import { CflsConfig, InitArgs, SerializableCflsConfig } from "./cflsTypes"; 9 | import * as Completions from "./completions"; 10 | import { getAttribute, getSourceFile } from "../compiler/utils"; 11 | import { BinaryOperator, BinaryOpType, BlockType, Diagnostic, FunctionDefinition, NodeKind, Property, SourceFile } from "../compiler/node"; 12 | import { SourceRange } from "../compiler/scanner"; 13 | import { EngineVersions } from "../compiler/engines"; 14 | import { TypeKind } from "../compiler/types"; 15 | 16 | declare const REPLACED_AT_BUILD : IREPLACED_AT_BUILD; 17 | 18 | // function send(msg: CflsResponse) { 19 | // process.send!(msg); 20 | // } 21 | 22 | // const languageTool = LanguageTool(); 23 | 24 | const NO_DATA = undefined; 25 | const CANCELLED = null; 26 | 27 | type Result = typeof NO_DATA | typeof CANCELLED | T; 28 | 29 | // process.on("message", (msg: CflsRequest) => { 30 | // let response : CflsResponse | undefined = undefined; 31 | // switch (msg.type) { 32 | // case CflsRequestType.init: { 33 | // languageTool.init(msg.initArgs); 34 | // response = {type: CflsResponseType.initialized, id: msg.id}; 35 | // break; 36 | // } 37 | // case CflsRequestType.diagnostics: { 38 | // const diagnostics = languageTool.naiveGetDiagnostics(msg.fsPath, msg.freshText, msg.sourceRange ?? undefined); 39 | // if (diagnostics === NO_DATA) { 40 | // break; 41 | // } 42 | // else if (diagnostics === CANCELLED) { 43 | // response = {type: CflsResponseType.cancelled, id: msg.id}; 44 | // } 45 | // else { 46 | // response = {type: CflsResponseType.diagnostics, id: msg.id, fsPath: diagnostics.fsPath, diagnostics: diagnostics.diagnostics}; 47 | // } 48 | // break; 49 | // } 50 | // case CflsRequestType.completions: { 51 | // const completions = languageTool.getCompletions(msg.fsPath, msg.targetIndex, msg.triggerCharacter); 52 | // if (completions === NO_DATA) { 53 | // break; 54 | // } 55 | // else if (completions === CANCELLED) { 56 | // response = {type: CflsResponseType.cancelled, id: msg.id}; 57 | // break; 58 | // } 59 | // else { 60 | // response = {type: CflsResponseType.completions, id: msg.id, fsPath: completions.fsPath, completionItems: completions.completionItems}; 61 | // break; 62 | // } 63 | // } 64 | // case CflsRequestType.reset: { 65 | // languageTool.reset(msg.config); 66 | // break; 67 | // } 68 | // case CflsRequestType.definitionLocations: { 69 | // const locations = languageTool.getDefinitionLocations(msg.fsPath, msg.targetIndex); 70 | // if (locations === NO_DATA) { 71 | // break; 72 | // } 73 | // else if (locations === CANCELLED) { 74 | // response = {type: CflsResponseType.cancelled, id: msg.id}; 75 | // break; 76 | // } 77 | // else { 78 | // response = {type: CflsResponseType.definitionLocations, id: msg.id, locations} 79 | // } 80 | // break; 81 | // } 82 | // default: { 83 | // exhaustiveCaseGuard(msg); 84 | // } 85 | // } 86 | 87 | // if (response) send(response); 88 | // }); 89 | 90 | type AbsPath = string; 91 | 92 | // function getClientAdapter() : ClientAdapter { 93 | // // adapter module is expected to `export const adapter : ClientAdapter = ...` 94 | // return require(REPLACED_AT_BUILD.ClientAdapterModule_StaticRequirePath).adapter; 95 | // } 96 | 97 | export function LanguageTool() { 98 | let config! : CflsConfig; 99 | let workspaceProjects! : Map; 100 | let workspaceRoots! : AbsPath[]; 101 | let cancellationToken: CancellationTokenConsumer; 102 | 103 | function init(initArgs : InitArgs) { 104 | workspaceProjects = new Map(); 105 | workspaceRoots = initArgs.workspaceRoots; 106 | cancellationToken = CancellationTokenConsumer(initArgs.cancellationTokenId) 107 | 108 | reset(initArgs.config); 109 | } 110 | 111 | function getOwningProjectFromAbsPath(absPath: AbsPath) : Project | undefined { 112 | for (const [workspaceRoot, project] of workspaceProjects) { 113 | const effectiveAbsPath = project.canonicalizePath(absPath); 114 | if (effectiveAbsPath.startsWith(workspaceRoot)) { 115 | return project; 116 | } 117 | } 118 | return undefined; 119 | } 120 | 121 | function naiveGetDiagnostics(fsPath: AbsPath, freshText: string, sourceRange?: {sourceRange: SourceRange, changeSize: number}) : Result<{fsPath: AbsPath, diagnostics: Diagnostic[]}> { 122 | const project = getOwningProjectFromAbsPath(fsPath); 123 | if (!project) return NO_DATA; 124 | 125 | /*const timing =*/ project.parseBindCheck(fsPath, freshText, sourceRange); 126 | 127 | if (cancellationToken.cancellationRequested()) return CANCELLED; 128 | //connection.console.info(`${fsPath}\n\tparse ${timing.parse} // bind ${timing.bind} // check ${timing.check}`); 129 | 130 | const diagnostics = project.getDiagnostics(fsPath) ?? []; 131 | const sourceFile = project.getParsedSourceFile(fsPath) ?? null; 132 | if (!sourceFile) return NO_DATA; 133 | 134 | return { 135 | fsPath, 136 | diagnostics 137 | } 138 | } 139 | 140 | function getCompletions(fsPath: AbsPath, targetIndex: number, triggerCharacter: string | null) : Result<{fsPath: AbsPath, completions: Completions.CompletionItem[]}> { 141 | const project = getOwningProjectFromAbsPath(fsPath); 142 | if (!project) return NO_DATA; 143 | 144 | const file = project.getParsedSourceFile(fsPath); 145 | if (!file) return NO_DATA; 146 | 147 | const completions = Completions.getCompletions( 148 | project, 149 | fsPath, 150 | targetIndex, 151 | triggerCharacter); 152 | 153 | return { 154 | fsPath, 155 | completions 156 | }; 157 | } 158 | 159 | const exactlyFirstCharRange = new SourceRange(0,0); 160 | type SourceLocation = {sourceFile: SourceFile, range: SourceRange}; 161 | 162 | function getDefinitionLocations(fsPath: AbsPath, targetIndex: number) : { sourceFile: Readonly, range: SourceRange}[] | undefined | null { 163 | return __getDefinitionLocations(); 164 | 165 | function __getDefinitionLocations() { // fixme: this isn't cancellable, so Result is unnecessary? 166 | const project = getOwningProjectFromAbsPath(fsPath); 167 | if (!project) return undefined; 168 | 169 | const sourceFile = project.__unsafe_dev_getFile(fsPath); 170 | if (!sourceFile) return undefined; 171 | 172 | const targetNode = project.getNodeContainingIndex_forSourceLocation(fsPath, targetIndex); 173 | if (!targetNode) return undefined; 174 | 175 | const checker = project.__unsafe_dev_getChecker(); 176 | 177 | // someType function foo() {} 178 | // ^^^^^^^^ 179 | if (targetNode.parent?.kind === NodeKind.functionDefinition && !targetNode.parent.fromTag && targetNode.parent.returnType === targetNode) { 180 | const symbol = checker.getSymbol(targetNode.parent, sourceFile) 181 | if (symbol && symbol.symTabEntry.firstLexicalType?.kind === TypeKind.functionSignature && symbol.symTabEntry.firstLexicalType.returns.kind === TypeKind.cfc) { 182 | return [{ 183 | sourceFile: symbol.symTabEntry.firstLexicalType.returns.cfc, 184 | range: exactlyFirstCharRange, 185 | }]; 186 | } 187 | return undefined; 188 | } 189 | 190 | if (targetNode.kind === NodeKind.simpleStringLiteral) { 191 | if (targetNode.parent?.kind === NodeKind.tagAttribute && targetNode.parent.canonicalName === "extends") { 192 | if (targetNode.parent?.parent?.kind === NodeKind.block 193 | && targetNode.parent.parent.subType === BlockType.scriptSugaredTagCallBlock 194 | && targetNode.parent.parent.name?.token.text.toLowerCase() === "component") { 195 | // component extends="..." 196 | // ^^^^^ 197 | if (sourceFile.cfc?.extends) { 198 | return [{ 199 | sourceFile: sourceFile.cfc.extends, 200 | range: exactlyFirstCharRange, 201 | }] 202 | } 203 | 204 | } 205 | } 206 | return undefined; 207 | } 208 | 209 | const newExpr = targetNode.kind === NodeKind.dottedPathRest 210 | && targetNode.parent?.kind === NodeKind.dottedPath 211 | && targetNode.parent.parent?.kind === NodeKind.callExpression 212 | && targetNode.parent.parent.parent?.kind === NodeKind.new 213 | ? targetNode.parent.parent.parent 214 | : targetNode.kind === NodeKind.dottedPath 215 | && targetNode.parent?.kind === NodeKind.callExpression 216 | && targetNode.parent.parent?.kind === NodeKind.new 217 | ? targetNode.parent.parent 218 | : undefined; 219 | 220 | if (newExpr) { 221 | const type = checker.getCachedEvaluatedNodeType(newExpr, sourceFile); 222 | if (type && type.kind === TypeKind.cfc) { 223 | return [{ 224 | sourceFile: type.cfc, 225 | range: exactlyFirstCharRange 226 | }] 227 | } 228 | return undefined; 229 | } 230 | 231 | const symbol = checker.getSymbol(targetNode, sourceFile); 232 | if (!symbol || !symbol.symTabEntry.declarations) return undefined; 233 | 234 | const result : SourceLocation[] = []; 235 | 236 | for (const decl of symbol.symTabEntry.declarations) { 237 | const location = decl.kind === NodeKind.property 238 | ? getPropertyDefinitionLocation(decl) 239 | : decl.kind === NodeKind.functionDefinition 240 | ? getFunctionDefinitionLocation(decl) 241 | : decl.kind === NodeKind.binaryOperator 242 | ? getAssignmentLocation(decl) 243 | : undefined; 244 | 245 | if (!location) continue; 246 | 247 | result.push(location); 248 | } 249 | 250 | return result; 251 | 252 | function getAssignmentLocation(node: BinaryOperator) : SourceLocation | undefined { 253 | if (node.optype !== BinaryOpType.assign) return undefined; 254 | const declSourceFile = getSourceFile(node); 255 | if (!declSourceFile) return undefined; 256 | return { 257 | sourceFile: declSourceFile, 258 | range: node.left.range 259 | } 260 | } 261 | 262 | function getFunctionDefinitionLocation(node: FunctionDefinition) : SourceLocation | undefined { 263 | const sourceFile = getSourceFile(node); 264 | if (!sourceFile) return undefined; 265 | if (node.fromTag) { 266 | if (!node.tagOrigin?.startTag?.range) return undefined; 267 | return { 268 | sourceFile, 269 | range: node.tagOrigin.startTag.range 270 | } 271 | } 272 | else { 273 | if (!node.nameToken) return undefined; 274 | return { 275 | sourceFile, 276 | range: node.nameToken.range 277 | } 278 | } 279 | } 280 | 281 | function getPropertyDefinitionLocation(node: Property) : SourceLocation | undefined { 282 | const sourceFile = getSourceFile(node); 283 | if (!sourceFile) return undefined; 284 | if (node.fromTag) { 285 | return { 286 | sourceFile, 287 | range: node.range 288 | } 289 | } 290 | else { 291 | const nameAttr = getAttribute(node.attrs, "name"); 292 | if (!nameAttr) return undefined; 293 | return { 294 | sourceFile, 295 | range: nameAttr.name.range 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | function reset(freshConfig: SerializableCflsConfig) { 303 | config = {...freshConfig, engineVersion: EngineVersions[freshConfig.engineVersion]}; 304 | const fileSystem = FileSystem(); 305 | 306 | workspaceProjects.clear(); 307 | 308 | for (const workspace of workspaceRoots) { 309 | const rootAbsPath = fileSystem.caseSensitive ? workspace : workspace.toLowerCase(); 310 | const project = Project( 311 | rootAbsPath, 312 | fileSystem, 313 | { 314 | parseTypes: config.x_parseTypes, 315 | debug: REPLACED_AT_BUILD.debug, 316 | engineVersion: config.engineVersion, 317 | withWireboxResolution: config.wireboxResolution, 318 | cfConfigProjectRelativePath: config.cfConfigProjectRelativePath, 319 | checkReturnTypes: config.x_checkReturnTypes, 320 | checkFlowTypes: config.x_checkFlowTypes, 321 | genericFunctionInference: config.x_genericFunctionInference, 322 | cancellationToken 323 | }, 324 | ); 325 | 326 | if (config.engineLibAbsPath) { 327 | project.addEngineLib(config.engineLibAbsPath); 328 | } 329 | 330 | workspaceProjects.set(rootAbsPath, project); 331 | } 332 | } 333 | 334 | return { 335 | naiveGetDiagnostics, 336 | init, 337 | reset, 338 | getCompletions, 339 | getDefinitionLocations, 340 | } 341 | } -------------------------------------------------------------------------------- /test/mxunit-smoketest.spec.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import * as fs from "fs"; 3 | import * as path from "path"; 4 | import { Parser, Binder, cfmOrCfc, SourceFile, flattenTree, Checker, CfFileType } from "../src/compiler"; 5 | import { EngineVersions } from "../src/compiler/engines"; 6 | import { ProjectOptions } from "../src/compiler/project"; 7 | 8 | const expectedDiagnosticCountByFile : Record = { 9 | "./mxunit/mxunit-TestCase-Template.cfc": 0, 10 | "./mxunit/mxunit-TestSuiteTemplate.cfm": 0, 11 | "./mxunit/doc/build.cfm": 0, 12 | "./mxunit/doc/colddoc/ColdDoc.cfc": 0, 13 | "./mxunit/doc/colddoc/strategy/AbstractTemplateStrategy.cfc": 0, 14 | "./mxunit/doc/colddoc/strategy/api/HTMLAPIStrategy.cfc": 0, 15 | "./mxunit/doc/colddoc/strategy/uml2tools/XMIStrategy.cfc": 0, 16 | "./mxunit/framework/Assert.cfc": 0, 17 | "./mxunit/framework/ComponentBlender.cfc": 0, 18 | "./mxunit/framework/ComponentUtils.cfc": 0, 19 | "./mxunit/framework/ConfigManager.cfc": 0, 20 | "./mxunit/framework/CSVUtility.cfc": 0, 21 | "./mxunit/framework/DataCompare.cfc": 0, 22 | "./mxunit/framework/DataproviderHandler.cfc": 0, 23 | "./mxunit/framework/Formatters.cfc": 0, 24 | "./mxunit/framework/HamcrestAssert.cfc": 0, 25 | "./mxunit/framework/HamcrestMatcher.cfc": 0, 26 | "./mxunit/framework/HtmlTestResult.cfc": 0, 27 | "./mxunit/framework/JUnitXMLTestResult.cfc": 0, 28 | "./mxunit/framework/mail.cfc": 0, 29 | "./mxunit/framework/MockFactoryFactory.cfc": 0, 30 | "./mxunit/framework/MXUnitAssertionExtensions.cfc": 0, 31 | "./mxunit/framework/POIUtility.cfc": 0, 32 | "./mxunit/framework/PublicProxyMaker.cfc": 0, 33 | "./mxunit/framework/QueryTestResult.cfc": 0, 34 | "./mxunit/framework/RemoteFacade.cfc": 0, 35 | "./mxunit/framework/RemoteFacadeObjectCache.cfc": 0, 36 | "./mxunit/framework/Test.cfc": 0, 37 | "./mxunit/framework/TestCase.cfc": 0, 38 | "./mxunit/framework/TestDecorator.cfc": 0, 39 | "./mxunit/framework/TestResult.cfc": 0, 40 | "./mxunit/framework/TestSuite.cfc": 0, 41 | "./mxunit/framework/TestSuiteRunner.cfc": 0, 42 | "./mxunit/framework/TextTestResult.cfc": 0, 43 | "./mxunit/framework/VersionReader.cfc": 0, 44 | "./mxunit/framework/XMLTestResult.cfc": 0, 45 | "./mxunit/framework/XPathAssert.cfc": 0, 46 | "./mxunit/framework/adapters/cf9/PublicProxyMaker.cfc": 0, 47 | "./mxunit/framework/decorators/AlphabeticallyOrderedTestsDecorator.cfc": 0, 48 | "./mxunit/framework/decorators/DataProviderDecorator.cfc": 0, 49 | "./mxunit/framework/decorators/OrderedTestDecorator.cfc": 0, 50 | "./mxunit/framework/decorators/TransactionRollbackDecorator.cfc": 0, 51 | "./mxunit/framework/ext/AssertionExtensionTemplate.cfc": 0, 52 | "./mxunit/framework/javaloader/JavaCompiler.cfc": 0, 53 | "./mxunit/framework/javaloader/JavaLoader.cfc": 0, 54 | "./mxunit/framework/javaloader/JavaProxy.cfc": 0, 55 | "./mxunit/framework/mightymock/AbstractMock.cfc": 0, 56 | "./mxunit/framework/mightymock/ArgumentMatcher.cfc": 0, 57 | "./mxunit/framework/mightymock/MightyMock.cfc": 0, 58 | "./mxunit/framework/mightymock/MightyMockFactory.cfc": 0, 59 | "./mxunit/framework/mightymock/MockDebug.cfc": 0, 60 | "./mxunit/framework/mightymock/MockFactory.cfc": 0, 61 | "./mxunit/framework/mightymock/MockLogger.cfc": 0, 62 | "./mxunit/framework/mightymock/MockRegistry.cfc": 0, 63 | "./mxunit/framework/mightymock/OrderedExpectation.cfc": 0, 64 | "./mxunit/framework/mightymock/Verifier.cfc": 0, 65 | "./mxunit/generator/Application.cfm": 0, 66 | "./mxunit/generator/generate.cfm": 0, 67 | "./mxunit/generator/index.cfm": 0, 68 | "./mxunit/generator/lib_cfscript.cfm": 0, 69 | "./mxunit/generator/listFiles.cfm": 4, // a no longer valid use of taglike call statement cfparam("foo","bar"), where it now appears named arguments are required 70 | "./mxunit/PluginDemoTests/CFScriptExpectedExceptionTest.cfc": 0, 71 | "./mxunit/PluginDemoTests/CompareDialogTest.cfc": 0, 72 | "./mxunit/PluginDemoTests/ComplexExceptionTypeErrorTest.cfc": 0, 73 | "./mxunit/PluginDemoTests/DoubleMethodTest.cfc": 0, 74 | "./mxunit/PluginDemoTests/EmptyTest.cfc": 0, 75 | "./mxunit/PluginDemoTests/ExpectedExceptionTest.cfc": 0, 76 | "./mxunit/PluginDemoTests/FiveSecondTest.cfc": 0, 77 | "./mxunit/PluginDemoTests/HodgePodgeTest.cfc": 0, 78 | "./mxunit/PluginDemoTests/InvalidMarkupTest.cfc": 1, 79 | "./mxunit/PluginDemoTests/PrivateMethodTest.cfc": 0, 80 | "./mxunit/PluginDemoTests/run.cfm": 0, 81 | "./mxunit/PluginDemoTests/SingleFailureTest.cfc": 0, 82 | "./mxunit/PluginDemoTests/SingleMethodTest.cfc": 0, 83 | "./mxunit/PluginDemoTests/SomeObject.cfc": 0, 84 | "./mxunit/PluginDemoTests/ThrowsAnErrorTest.cfc": 0, 85 | "./mxunit/PluginDemoTests/inheritance/BaseTest.cfc": 0, 86 | "./mxunit/PluginDemoTests/inheritance/SomeDoublyExtendingTest.cfc": 0, 87 | "./mxunit/PluginDemoTests/inheritance/SomeExtendingTest.cfc": 0, 88 | "./mxunit/PluginDemoTests/SubDir/CFUnitStyleTest.cfc": 0, 89 | "./mxunit/PluginDemoTests/SubDir/AnotherSubDir/AnotherTest.cfc": 0, 90 | "./mxunit/PluginDemoTests/SubDir/AnotherSubDir/SomeComponentWithStuff.cfc": 0, 91 | "./mxunit/PluginDemoTests/SubDir/AnotherSubDir/SomeOtherTest.cfc": 0, 92 | "./mxunit/PluginDemoTests/SubDir/AnotherSubDir/TestSomething.cfc": 0, 93 | "./mxunit/PluginDemoTests/TestOrdering/AlphabeticallyOrderedTest.cfc": 0, 94 | "./mxunit/PluginDemoTests/TestOrdering/DefaultOrderedTest.cfc": 0, 95 | "./mxunit/PluginDemoTests/weirderrordemos/BustedConstructorTest.cfc": 0, 96 | "./mxunit/PluginDemoTests/weirderrordemos/BustedSetupTest.cfc": 0, 97 | "./mxunit/PluginDemoTests/weirderrordemos/BustedTearDownTest.cfc": 0, 98 | "./mxunit/PluginDemoTests/weirderrordemos/extends/Extends.cfc": 0, 99 | "./mxunit/PluginDemoTests/weirderrordemos/extends/SomeTest.cfc": 0, 100 | "./mxunit/resources/jquery/spark.cfm": 0, 101 | "./mxunit/resources/theme/footer.cfm": 0, 102 | "./mxunit/resources/theme/header.cfm": 0, 103 | "./mxunit/runner/DirectoryTestSuite.cfc": 0, 104 | "./mxunit/runner/HtmlRunner.cfc": 0, 105 | "./mxunit/runner/HttpAntRunner.cfc": 0, 106 | "./mxunit/runner/index.cfm": 0, 107 | "./mxunit/runner/RunnerUtils.cfc": 0, 108 | "./mxunit/samples/DirectoryTestSuiteSample.cfm": 0, 109 | "./mxunit/samples/HttpAntRunner.cfc": 0, 110 | "./mxunit/samples/MyComponent.cfc": 0, 111 | "./mxunit/samples/MyComponentTest.cfc": 0, 112 | "./mxunit/samples/MyOtherComponentTest.cfc": 0, 113 | "./mxunit/samples/MyTestSuite.cfm": 0, 114 | "./mxunit/samples/PluginSimulator.cfm": 0, 115 | "./mxunit/samples/RemoteFacadeTester.cfm": 0, 116 | "./mxunit/samples/samples.cfm": 0, 117 | "./mxunit/samples/ScheduledRun.cfm": 0, 118 | "./mxunit/samples/SimpleRunSkeleton.cfm": 0, 119 | "./mxunit/samples/TestCaseSkeleton.cfc": 0, 120 | "./mxunit/samples/mocking/querysim.cfm": 0, 121 | "./mxunit/samples/mocking/TheCollaborator.cfc": 0, 122 | "./mxunit/samples/mocking/TheComponent.cfc": 0, 123 | "./mxunit/samples/mocking/TheMockTest.cfc": 0, 124 | "./mxunit/samples/mocking/TheStubTest.cfc": 0, 125 | "./mxunit/samples/tests/MyComponentTest.cfc": 0, 126 | "./mxunit/samples/tests/myTestSuite.cfm": 0, 127 | "./mxunit/samples/tests/TestCaseSkeletonTest.cfc": 0, 128 | "./mxunit/tests/run.cfm": 0, 129 | "./mxunit/tests/bugs/105.cfc": 0, 130 | "./mxunit/tests/bugs/105ExtendedTest.cfc": 0, 131 | "./mxunit/tests/bugs/149Test.cfc": 0, 132 | "./mxunit/tests/bugs/80.cfc": 0, 133 | "./mxunit/tests/bugs/90.cfc": 0, 134 | "./mxunit/tests/bugs/93.cfc": 0, 135 | "./mxunit/tests/bugs/Bug115.cfc": 0, 136 | "./mxunit/tests/bugs/bug126.cfc": 0, 137 | "./mxunit/tests/bugs/ExpectedExceptionBug147Test.cfc": 0, 138 | "./mxunit/tests/bugs/fixture/93sample.cfc": 0, 139 | "./mxunit/tests/bugs/fixture/test-with_hyphen.cfc": 0, 140 | "./mxunit/tests/bugs/fixture/test_with_underscore.cfc": 0, 141 | "./mxunit/tests/bugs/fixture/122/GoodTest.cfc": 0, 142 | "./mxunit/tests/bugs/fixture/122/ParseErrorTest.cfc": 1, 143 | "./mxunit/tests/bugs/run-me/test-with_hyphen.cfc": 0, 144 | "./mxunit/tests/bugs/run-me/test_with_underscore.cfc": 0, 145 | "./mxunit/tests/compatability/DeepStructureCompareTest.cfc": 0, 146 | "./mxunit/tests/compatability/DoesNotHaveTestAtEndOrBegining.cfc": 0, 147 | "./mxunit/tests/framework/AssertDecoratorTest.cfc": 0, 148 | "./mxunit/tests/framework/AssertionChainingTest.cfc": 0, 149 | "./mxunit/tests/framework/AssertSameTest.cfc": 0, 150 | "./mxunit/tests/framework/AssertTest.cfc": 0, 151 | "./mxunit/tests/framework/ComponentBlenderTest.cfc": 0, 152 | "./mxunit/tests/framework/ComponentUtilsTest.cfc": 0, 153 | "./mxunit/tests/framework/ConfigManagerTest.cfc": 0, 154 | "./mxunit/tests/framework/CSVUtilityTest.cfc": 0, 155 | "./mxunit/tests/framework/DataProviderTest.cfc": 0, 156 | "./mxunit/tests/framework/DynamicTestCaseGenerationTest.cfc": 2, // they have a tag in a component preamble, is that legit ? 157 | "./mxunit/tests/framework/ExpectedExceptionTest.cfc": 0, 158 | "./mxunit/tests/framework/HamcrestMatcherTest.cfc": 0, 159 | "./mxunit/tests/framework/HamcrestTest.cfc": 0, 160 | "./mxunit/tests/framework/HtmlTestResultTest.cfc": 0, 161 | "./mxunit/tests/framework/MockIntegrationTest.cfc": 0, 162 | "./mxunit/tests/framework/MXUnitAssertionExtensionsTest.cfc": 0, 163 | "./mxunit/tests/framework/PublicProxyMakerTest.cfc": 0, 164 | "./mxunit/tests/framework/querysim.cfm": 0, 165 | "./mxunit/tests/framework/QueryTestResultTest.cfc": 0, 166 | "./mxunit/tests/framework/RemoteFacadeObjectCacheTest.cfc": 6, // all errors are associated with the now-illegal use of 'final' as an identifier 167 | "./mxunit/tests/framework/RemoteFacadeTest.cfc": 0, 168 | "./mxunit/tests/framework/TagSoupTest.cfc": 0, 169 | "./mxunit/tests/framework/TestCaseBeforeAfterTest.cfc": 0, 170 | "./mxunit/tests/framework/TestCaseExtendsTest.cfc": 0, 171 | "./mxunit/tests/framework/TestCaseTest.cfc": 0, 172 | "./mxunit/tests/framework/TestDecoratorTest.cfc": 0, 173 | "./mxunit/tests/framework/TestResultTest.cfc": 0, 174 | "./mxunit/tests/framework/TestSuiteTest.cfc": 0, 175 | "./mxunit/tests/framework/TestTest.cfc": 0, 176 | "./mxunit/tests/framework/VersionReaderTest.cfc": 0, 177 | "./mxunit/tests/framework/XPathAssertionTest.cfc": 0, 178 | "./mxunit/tests/framework/adapters/cf9/PublicProxyMakerTest.cfc": 0, 179 | "./mxunit/tests/framework/fixture/ATestSuite.cfc": 0, 180 | "./mxunit/tests/framework/fixture/ComparatorTestData.cfc": 0, 181 | "./mxunit/tests/framework/fixture/ComponentWithPrivateMethods.cfc": 0, 182 | "./mxunit/tests/framework/fixture/DataProviderFixture.cfc": 0, 183 | "./mxunit/tests/framework/fixture/MockFactory.cfc": 0, 184 | "./mxunit/tests/framework/fixture/Mocking.cfc": 0, 185 | "./mxunit/tests/framework/fixture/mxunit-TestCase-Template.cfc": 0, 186 | "./mxunit/tests/framework/fixture/MyCFC.cfc": 0, 187 | "./mxunit/tests/framework/fixture/MyCFCTest.cfc": 0, 188 | "./mxunit/tests/framework/fixture/NewCFComponent.cfc": 0, 189 | "./mxunit/tests/framework/fixture/ParentWithPrivateMethods.cfc": 0, 190 | "./mxunit/tests/framework/fixture/querysim.cfm": 0, 191 | "./mxunit/tests/framework/fixture/TestAssertComponent.cfc": 0, 192 | "./mxunit/tests/framework/fixture/TestWithExpectedExceptionAttributes.cfc": 0, 193 | "./mxunit/tests/framework/fixture/decorators/IgnoreFunnyFunctionsDecorator.cfc": 0, 194 | "./mxunit/tests/framework/fixture/decorators/StoreTestNameDecorator.cfc": 0, 195 | "./mxunit/tests/framework/fixture/fixturetests/AnotherRandomTest.cfc": 0, 196 | "./mxunit/tests/framework/fixture/fixturetests/AnotherRandomTests.cfc": 0, 197 | "./mxunit/tests/framework/fixture/fixturetests/SomeRandomTest.cfc": 0, 198 | "./mxunit/tests/framework/fixture/fixturetests/SubClassWithNoMethodsTest.cfc": 0, 199 | "./mxunit/tests/framework/fixture/fixturetests/SuperClassWithPrivateMethodsTest.cfc": 0, 200 | "./mxunit/tests/framework/fixture/interfaces/AComponent.cfc": 0, 201 | "./mxunit/tests/framework/fixture/interfaces/AnInterface.cfc": 0, 202 | "./mxunit/tests/framework/fixture/interfaces/OtherInterface.cfc": 0, 203 | "./mxunit/tests/framework/fixture/interfaces/SubInterface.cfc": 0, 204 | "./mxunit/tests/install/cfcproxytest.cfc": 0, 205 | "./mxunit/tests/install/fixture/index.cfm": 0, 206 | "./mxunit/tests/install/fixture/test.cfm": 0, 207 | "./mxunit/tests/mightymock/AbstractMockTest.cfc": 0, 208 | "./mxunit/tests/mightymock/ArgumentMatcherTest.cfc": 0, 209 | "./mxunit/tests/mightymock/BaseTest.cfc": 0, 210 | "./mxunit/tests/mightymock/BasicMXUnitIntegrationTest.cfc": 0, 211 | "./mxunit/tests/mightymock/CaseSensitivtyTest.cfc": 0, 212 | "./mxunit/tests/mightymock/FileDeleterTest.cfc": 0, 213 | "./mxunit/tests/mightymock/InvocationTest.cfc": 0, 214 | "./mxunit/tests/mightymock/MightyMockTest.cfc": 0, 215 | "./mxunit/tests/mightymock/MockDebugTest.cfc": 0, 216 | "./mxunit/tests/mightymock/MockifyTest.cfc": 0, 217 | "./mxunit/tests/mightymock/MockInstantiationTest.cfc": 0, 218 | "./mxunit/tests/mightymock/MockLoggerTest.cfc": 0, 219 | "./mxunit/tests/mightymock/MockPlayTest.cfc": 0, 220 | "./mxunit/tests/mightymock/MockRegistryTest.cfc": 0, 221 | "./mxunit/tests/mightymock/MockVerificationTest.cfc": 0, 222 | "./mxunit/tests/mightymock/ObjectChainingAndReferenceTest.cfc": 0, 223 | "./mxunit/tests/mightymock/OrderTest.cfc": 0, 224 | "./mxunit/tests/mightymock/PatternInvocationTest.cfc": 0, 225 | "./mxunit/tests/mightymock/QueryNewTest.cfc": 0, 226 | "./mxunit/tests/mightymock/querysim.cfm": 0, 227 | "./mxunit/tests/mightymock/ReturnTypeTest.cfc": 0, 228 | "./mxunit/tests/mightymock/StateTransitionTest.cfc": 0, 229 | "./mxunit/tests/mightymock/TypeParserTest.cfc": 0, 230 | "./mxunit/tests/mightymock/VerfierTest.cfc": 0, 231 | "./mxunit/tests/mightymock/WilcardPatternTest.cfc": 0, 232 | "./mxunit/tests/mightymock/fixture/AcceptStrictType.cfc": 0, 233 | "./mxunit/tests/mightymock/fixture/Dummy.cfc": 0, 234 | "./mxunit/tests/mightymock/fixture/FileDeleter.cfc": 0, 235 | "./mxunit/tests/mightymock/fixture/Helper.cfc": 0, 236 | "./mxunit/tests/mightymock/fixture/Logger.cfc": 0, 237 | "./mxunit/tests/mightymock/fixture/Mockery.cfc": 0, 238 | "./mxunit/tests/mightymock/fixture/Mockify.cfc": 0, 239 | "./mxunit/tests/mightymock/fixture/MyComponent.cfc": 0, 240 | "./mxunit/tests/mightymock/fixture/MySpyObject.cfc": 0, 241 | "./mxunit/tests/mightymock/fixture/ParentSpyObject.cfc": 0, 242 | "./mxunit/tests/runner/DirectoryTestSuiteTest.cfc": 0, 243 | "./mxunit/tests/runner/HTMLRunnerTest.cfc": 0, 244 | "./mxunit/tests/runner/HttpAntRunnerTest.cfc": 0, 245 | "./mxunit/tests/samples/MyComponent.cfc": 0, 246 | "./mxunit/tests/samples/MyComponentTest.cfc": 0, 247 | "./mxunit/tests/samples/MyOtherComponentTest.cfc": 0, 248 | "./mxunit/tests/utils/TestBubbleSort.cfc": 0, // during checking: cannot find name assertequals - this is in the parent component 249 | "./mxunit/utils/BubbleSort.cfc": 0, 250 | }; 251 | 252 | describe("MX-Unit smoke test", () => { 253 | const options : ProjectOptions = { 254 | debug: true, 255 | parseTypes: false, 256 | withWireboxResolution: false, 257 | cfConfigProjectRelativePath: "", 258 | engineVersion: EngineVersions["acf.2018"], 259 | genericFunctionInference: false, 260 | checkReturnTypes: false, 261 | checkFlowTypes: false, 262 | cancellationToken: { 263 | cancellationRequested: () => false, 264 | throwIfCancellationRequested: () => void 0, 265 | } 266 | }; 267 | const parser = Parser(options); 268 | const binder = Binder(options) 269 | const checker = Checker(options); 270 | 271 | const libPath = path.resolve("./src/lang-server/server/src/runtimelib/lib.cf2018.d.cfm"); 272 | const stdLib = SourceFile(libPath , CfFileType.dCfm, fs.readFileSync(libPath)); 273 | parser.parse(stdLib); 274 | binder.bind(stdLib); 275 | 276 | for (const fileBaseName of Object.keys(expectedDiagnosticCountByFile)) { 277 | const expectedDiagnosticCount = expectedDiagnosticCountByFile[fileBaseName]; 278 | 279 | it(`Should parse ${fileBaseName} with exactly ___${expectedDiagnosticCount}___ emitted diagnostics`, () => { 280 | const absPath = path.resolve(__dirname, fileBaseName); 281 | const textBuffer = fs.readFileSync(absPath); 282 | const sourceFile = SourceFile(absPath, cfmOrCfc(absPath)!, textBuffer); 283 | parser.parse(sourceFile); 284 | 285 | flattenTree(sourceFile); // just make sure it doesn't throw 286 | binder.bind(sourceFile); 287 | //checker.check(sourceFile, parser.getScanner(), parser.getDiagnostics()); 288 | 289 | assert.strictEqual(sourceFile.diagnostics.length, expectedDiagnosticCount, `${fileBaseName} parsed with exactly ${expectedDiagnosticCount} emitted diagnostics`); 290 | }); 291 | } 292 | }); -------------------------------------------------------------------------------- /src/services/completions.ts: -------------------------------------------------------------------------------- 1 | // fixme - use non-relative paths, which requires we get ts-node to resolve the paths during testing 2 | // we can get it to compile with tsc with non-relative paths, but loading it during testing does not work 3 | import { Project } from "../compiler/project" 4 | import { Node, NodeKind, CallExpression, CfTag, StaticallyKnownScopeName, SymbolTable, SymTabEntry, SimpleStringLiteral, SourceFile } from "../compiler/node" 5 | import { CfFileType, SourceRange, TokenType } from "../compiler/scanner"; 6 | import { cfFunctionSignatureParam, SymbolTableTypeWrapper, TypeFlags, Type, TypeKind, BuiltinType, isStructLikeOrArray, cfFunctionSignature } from "../compiler/types"; 7 | import { isExpressionContext, isCfScriptTagBlock, stringifyCallExprArgName, getSourceFile, cfcIsDescendantOf, isPublicMethod, getTriviallyComputableString } from "../compiler/utils"; 8 | import { Checker } from "../compiler/checker"; 9 | 10 | export const enum CompletionItemKind { 11 | tagName, 12 | function, 13 | variable, 14 | structMember, 15 | stringLiteral 16 | } 17 | 18 | interface InsertReplaceEdit { 19 | newText: string; 20 | /** 21 | * The range if the insert is requested 22 | */ 23 | insert: SourceRange; 24 | /** 25 | * The range if the replace is requested. 26 | */ 27 | replace: SourceRange; 28 | range: SourceRange; 29 | } 30 | 31 | export interface CompletionItem { 32 | label: string, 33 | kind: CompletionItemKind, 34 | detail?: string, 35 | insertText?: string, 36 | sortText?: string, 37 | textEdit?: InsertReplaceEdit 38 | } 39 | 40 | function getCallExprArgIndex(callExpr: CallExpression, node: Node) { 41 | const index = callExpr.args.findIndex((v) => v.expr === node); 42 | return index === -1 ? undefined : index; 43 | } 44 | 45 | function getParamTypeByName(signature: cfFunctionSignature, canonicalArgName: string) : Type | undefined { 46 | for (const param of signature.params) { 47 | if (param.canonicalName === canonicalArgName) { 48 | return param.paramType; 49 | } 50 | } 51 | return undefined; 52 | } 53 | 54 | function extractTopLevelStringsFromType(type: Type) : string[] { 55 | switch (type.kind) { 56 | case TypeKind.union: { 57 | return type.types.map(extractTopLevelStringsFromType).flat(); 58 | } 59 | case TypeKind.literal: { 60 | if (typeof type.literalValue === "string") { 61 | return [type.literalValue]; 62 | } 63 | return []; 64 | } 65 | default: { 66 | return []; 67 | } 68 | } 69 | } 70 | 71 | function getStringLiteralCompletions(checker: Checker, sourceFile: SourceFile, node: SimpleStringLiteral) : CompletionItem[] | undefined { 72 | const strings = new Set(); 73 | 74 | if (node.parent?.kind === NodeKind.callArgument && node.parent.parent?.kind === NodeKind.callExpression) { 75 | if (node.parent.name) { 76 | const canonicalParamName = getTriviallyComputableString(node.parent.name)?.toLowerCase(); 77 | if (!canonicalParamName) return []; 78 | const calledSignature = checker.getCachedEvaluatedNodeType(node.parent.parent.left, sourceFile); 79 | if (!calledSignature) return undefined; 80 | 81 | if (calledSignature.kind === TypeKind.functionOverloadSet) { // this was primarily to investigate wirebox typename completions in `getInstance` 82 | // for (const overload of type.overloads) { 83 | // const type = overload.params.find(param => param.canonicalName === paramName)?.paramType; 84 | // if (!type) continue; 85 | // if (type.kind === TypeKind.literal && type.underlyingType === BuiltinType.string) { 86 | // strings.add(type.literalValue as string); 87 | // } 88 | // } 89 | return undefined; 90 | } 91 | else if (calledSignature.kind === TypeKind.functionSignature) { 92 | const paramType = getParamTypeByName(calledSignature, canonicalParamName); 93 | if (!paramType) { 94 | return undefined; 95 | } 96 | return extractTopLevelStringsFromType(paramType).map((s) => ({label: s, kind: CompletionItemKind.stringLiteral})); 97 | } 98 | else { 99 | // no-op -- ? 100 | } 101 | } 102 | else { 103 | const ziArgIndex = getCallExprArgIndex(node.parent.parent, node); 104 | if (ziArgIndex === undefined) return undefined; 105 | // const symbol = checker.getSymbol(node.parent.parent.left, sourceFile); 106 | // if (!symbol) return undefined; 107 | const type = checker.getCachedEvaluatedNodeType(node.parent.parent.left, sourceFile); 108 | if (!type) return undefined; 109 | 110 | if (type.kind === TypeKind.functionOverloadSet) { // this was primarily to investigate wirebox typename completions in `getInstance` 111 | // for (const overload of type.overloads) { 112 | // const type = overload.params[ziArgIndex]?.paramType; 113 | // if (!type) continue; 114 | // if (type.kind === TypeKind.literal && type.underlyingType === BuiltinType.string) { 115 | // strings.add(type.literalValue as string); 116 | // } 117 | // } 118 | } 119 | else if (type.kind === TypeKind.functionSignature) { 120 | return undefined; // not yet impl'd 121 | } 122 | else if (type.kind === TypeKind.genericFunctionSignature) { 123 | const argType = type.params[ziArgIndex]?.paramType; 124 | if (!argType || argType.kind !== TypeKind.typeId) { 125 | return undefined; 126 | } 127 | const typeParam = type.typeParams.find(typeParam => typeParam.name === argType.name); 128 | if (!typeParam) { 129 | return undefined; 130 | } 131 | if (typeParam.extends?.kind === TypeKind.keyof && typeParam.extends.concrete) { 132 | for (const keyName of typeParam.extends.keyNames) { 133 | strings.add(keyName); 134 | } 135 | } 136 | 137 | // no-op -- ? 138 | } 139 | } 140 | } 141 | else if (node.parent?.kind === NodeKind.tagAttribute) { 142 | const attrType = checker.getCachedEvaluatedNodeType(node.parent, sourceFile); 143 | if (attrType.kind === TypeKind.union) { 144 | 145 | for (const member of attrType.types) { 146 | if (member.kind === TypeKind.literal && member.underlyingType === BuiltinType.string) { 147 | strings.add(member.literalValue as string); 148 | } 149 | if (member === BuiltinType.boolean) { 150 | strings.add("yes"); 151 | strings.add("no"); 152 | } 153 | } 154 | } 155 | } 156 | 157 | if (strings.size === 0) { 158 | return undefined; 159 | } 160 | else { 161 | const result : CompletionItem[] = []; 162 | strings.forEach((s) => { 163 | //const v = new SourceRange(node.range.fromInclusive+1, node.range.fromInclusive + s.length); 164 | result.push({ 165 | label: s, 166 | kind: CompletionItemKind.stringLiteral, 167 | // textEdit: { 168 | // newText: s, 169 | // range: v, 170 | // insert: v, 171 | // replace: v 172 | // } 173 | }); 174 | }); 175 | 176 | return result.sort((l,r) => l.label < r.label ? -1 : l.label === r.label ? 0 : 1); 177 | } 178 | } 179 | 180 | export function getCompletions(project: Project, fsPath: string, targetIndex: number, triggerCharacter: string | null) : CompletionItem[] { 181 | if (!project) return []; 182 | 183 | const parsedSourceFile = project.getParsedSourceFile(fsPath); 184 | const checker = project.__unsafe_dev_getChecker(); 185 | const node = project.getNodeToLeftOfCursor_forCompletion(fsPath, targetIndex); // fixme: probably generally want "getInterestingNodeLeftOfCursor" to not grab terminals, but all the ".parent.parent..." chains would have to be fixed up 186 | 187 | if (!parsedSourceFile || !node) return []; 188 | 189 | // check that targetIndex is inside the range of the stringLiteral, because trailing trivia is bound to the string literal 190 | // So, `"abcdefg" ` 191 | // the text-span's parent is the string literal, but we obviously don't want to offer completions from that position 192 | if ((node.kind === NodeKind.terminal || node.kind === NodeKind.textSpan) && node.parent?.kind === NodeKind.simpleStringLiteral && node.parent.range.includes(targetIndex)) { 193 | const checker = project.__unsafe_dev_getChecker(); 194 | const sourceFile = getSourceFile(node); 195 | if (checker && sourceFile) { 196 | return getStringLiteralCompletions(checker, sourceFile, node.parent) ?? []; 197 | } 198 | else { 199 | return []; 200 | } 201 | } 202 | // after the above, we've handled all the string completions 203 | if (triggerCharacter === "\"" || triggerCharacter === "'") { 204 | return []; 205 | } 206 | 207 | const expressionContext = isExpressionContext(node); 208 | 209 | if (!expressionContext) { 210 | const maybeTagCall = node.parent?.kind === NodeKind.terminal && node.parent.parent?.kind === NodeKind.tag 211 | ? node.parent.parent 212 | : node.parent?.kind === NodeKind.tagAttribute && node.parent.parent?.kind === NodeKind.tag 213 | ? node.parent.parent 214 | : node.parent?.kind === NodeKind.terminal && node.parent.parent?.kind === NodeKind.tagAttribute && node.parent.parent.parent?.kind === NodeKind.tag 215 | ? node.parent.parent.parent 216 | : node.parent?.kind === NodeKind.terminal && node.parent.parent?.parent?.kind === NodeKind.tagAttribute && node.parent.parent.parent.parent?.kind === NodeKind.tag 217 | ? node.parent.parent.parent.parent 218 | : null; 219 | 220 | if (!maybeTagCall) { 221 | return []; 222 | } 223 | 224 | const sig = checker.getCachedEvaluatedNodeType(maybeTagCall, parsedSourceFile); 225 | if (sig.kind !== TypeKind.functionSignature) { 226 | return []; 227 | } 228 | 229 | return namedCallArgumentCompletions(sig.params, new Set(sig.params.map(param => param.canonicalName)), "Tag attribute"); 230 | } 231 | 232 | const callExpr : CallExpression | null = (node.parent?.parent?.kind === NodeKind.callArgument && !node.parent.parent.equals) 233 | ? node.parent.parent.parent as CallExpression // inside a named argument `foo(a|) 234 | : (node.kind === NodeKind.terminal && node.token.type === TokenType.LEFT_PAREN && node.parent?.kind === NodeKind.callExpression) 235 | ? node.parent as CallExpression // right on `foo(|` 236 | : (node.parent?.kind === NodeKind.terminal && node.parent.token.type === TokenType.LEFT_PAREN && node.parent.parent?.kind === NodeKind.callExpression) 237 | ? node.parent.parent // on whitespace after `foo( |` 238 | : (node.parent?.kind === NodeKind.terminal && node.parent.token.type === TokenType.COMMA && node.parent.parent?.parent?.kind === NodeKind.callExpression) 239 | ? node.parent.parent.parent // after a comma `foo(arg0, |` 240 | : null; 241 | 242 | // a whitespace or left-paren trigger character is only used for showing named parameters inside a call argument list 243 | if ((triggerCharacter === " " || triggerCharacter === "(") && !callExpr) { 244 | if (!callExpr) return []; 245 | } 246 | 247 | if (isCfScriptTagBlock(node)) { 248 | let justCfScriptCompletion = false; 249 | // if we got = 2) { 255 | const text = parsedSourceFile.scanner.getSourceText(); 256 | if (text[node.range.fromInclusive-2] === "<" && text[node.range.fromInclusive-1] === "/") { 257 | justCfScriptCompletion = true; 258 | } 259 | } 260 | 261 | if (justCfScriptCompletion) { 262 | return [{ 263 | label: "cfscript", 264 | kind: CompletionItemKind.tagName, 265 | detail: "cflsp:<>", 266 | insertText: "cfscript>", 267 | }]; 268 | } 269 | } 270 | 271 | const result : CompletionItem[] = []; 272 | 273 | // fixme: hoist out 274 | function namedCallArgumentCompletions(params: readonly cfFunctionSignatureParam[], yetToBeUsedParams: ReadonlySet, detail: string) : CompletionItem[] { 275 | const result : CompletionItem[] = []; 276 | for (const param of params) { 277 | if (!yetToBeUsedParams.has(param.canonicalName)) continue; 278 | if (!param.uiName) continue; 279 | if (param.flags & TypeFlags.spread) continue; // don't show a name for a spread arg 280 | result.push({ 281 | label: param.uiName + "=", 282 | kind: CompletionItemKind.variable, 283 | detail: detail, 284 | sortText: "000_" + param.uiName, // we'd like param name suggestions first 285 | }); 286 | } 287 | return result; 288 | } 289 | 290 | // `foo(bar = baz, |)` 291 | // `foo(b|` 292 | // `foo( |)` 293 | // NOT `foo(bar = |`, that should be an expression completion 294 | if (callExpr) { 295 | const sig = checker.getCachedEvaluatedNodeType(callExpr.left, parsedSourceFile); 296 | if (sig.kind === TypeKind.functionOverloadSet) return []; 297 | if (sig.kind === TypeKind.functionSignature) { 298 | const yetToBeUsedParams = new Set(sig.params.map(param => param.canonicalName)); 299 | for (const arg of callExpr.args) { 300 | const argName = stringifyCallExprArgName(arg); 301 | if (argName) yetToBeUsedParams.delete(argName.canonical); 302 | } 303 | 304 | const detail = callExpr.parent?.kind === NodeKind.new ? "named constructor argument" : "named function argument"; 305 | result.push(...namedCallArgumentCompletions(sig.params, yetToBeUsedParams, detail)); 306 | } 307 | } 308 | 309 | if (node.kind === NodeKind.indexedAccessChainElement || node.parent?.kind === NodeKind.indexedAccessChainElement) { 310 | // get the type one level before the current 311 | // x.y| --> for x 312 | // x.| --> for x 313 | // x.y.| --> for x.y 314 | const targetIndexedAccessNode = (() => { 315 | if (node.kind === NodeKind.indexedAccessChainElement) { 316 | return node.parent; 317 | } 318 | else if (node.parent?.kind === NodeKind.indexedAccessChainElement) { 319 | return node.parent.parent; 320 | } 321 | else { 322 | throw "unreachable"; 323 | } 324 | })(); 325 | 326 | // fixme: unify "type" vs. "SymbolTable" disparity, we have symbol tables pretending to be structs for typechecking purposes but is that necessary? or can everything be a struct or ... ? 327 | let typeinfo : Type | SymbolTable | undefined = project.__unsafe_dev_getChecker().getCachedEvaluatedNodeType(targetIndexedAccessNode, parsedSourceFile); 328 | 329 | // the first symbol table we get is a cfStruct or null; after that, we will start getting actual symbol tables 330 | let parsedSourceFileIsDescendantOfTypeinfoCfc : boolean; 331 | let workingSourceFile : SourceFile; 332 | 333 | if (typeinfo.kind === TypeKind.cfc) { 334 | parsedSourceFileIsDescendantOfTypeinfoCfc = cfcIsDescendantOf(typeinfo.cfc, parsedSourceFile); 335 | workingSourceFile = typeinfo.cfc; 336 | } 337 | else { 338 | parsedSourceFileIsDescendantOfTypeinfoCfc = false; 339 | workingSourceFile = parsedSourceFile; 340 | } 341 | 342 | const result : CompletionItem[] = []; 343 | 344 | while (typeinfo) { 345 | let underlyingMembers : ReadonlyMap; 346 | let interfaceExtension : ReadonlyMap | undefined = undefined; 347 | let currentStructLikeIsCfc = false; 348 | 349 | if (typeinfo instanceof Map) { 350 | currentStructLikeIsCfc = true; // we got a symboltable, which we only get in cases of climbing into a parent CFC 351 | underlyingMembers = typeinfo; 352 | if (workingSourceFile.cfFileType === CfFileType.cfc && workingSourceFile.containedScope.typeinfo.mergedInterfaces.has("this")) { 353 | interfaceExtension = workingSourceFile.containedScope.typeinfo.mergedInterfaces.get("this")!.members; 354 | } 355 | } 356 | else if (isStructLikeOrArray(typeinfo)) { 357 | underlyingMembers = typeinfo.members; 358 | currentStructLikeIsCfc = typeinfo.kind === TypeKind.cfc; 359 | if (typeinfo.kind === TypeKind.symbolTableTypeWrapper) interfaceExtension = (typeinfo as SymbolTableTypeWrapper).interfaceExtension?.members; 360 | } 361 | else { 362 | break; // unreachable 363 | } 364 | 365 | const runOne = (symTabEntry: SymTabEntry) => { 366 | if (symTabEntry.canonicalName === "init" && currentStructLikeIsCfc) return; // don't need to show init, we can still check its signature though? 367 | if ( currentStructLikeIsCfc && symTabEntry.firstLexicalType?.kind === TypeKind.functionSignature && !isPublicMethod(symTabEntry.firstLexicalType)) { 368 | if (!parsedSourceFileIsDescendantOfTypeinfoCfc) return; // don't offer completions for non-public members for non-descendants 369 | } 370 | const effectiveType = symTabEntry.links?.effectiveDeclaredType ?? symTabEntry.firstLexicalType; 371 | result.push({ 372 | label: symTabEntry.uiName, 373 | kind: effectiveType?.kind === TypeKind.functionSignature || effectiveType?.kind === TypeKind.genericFunctionSignature 374 | ? CompletionItemKind.function 375 | : CompletionItemKind.structMember, 376 | detail: "" 377 | }) 378 | } 379 | 380 | underlyingMembers.forEach(runOne); 381 | interfaceExtension?.forEach(runOne); 382 | 383 | // climb the hierarchy and offer public members of parent components, too 384 | if (currentStructLikeIsCfc && workingSourceFile.cfc?.extends) { 385 | workingSourceFile = workingSourceFile.cfc.extends; 386 | typeinfo = workingSourceFile.containedScope.this; // the first typeinfo was guaranteed to be a Struct; from here and for every subsequent iteration, we get a SymbolTable instaed 387 | } 388 | else { 389 | break; 390 | } 391 | } 392 | 393 | return result; 394 | } 395 | 396 | // we're in a primary expression context, where we need to do symbol lookup in all visible scopes 397 | // e.g,. `x = | + y` 398 | const allVisibleNames = (function (node: Node | null) { 399 | const result = new Map(); 400 | let scopeDistance = 0; // keep track of "how far away" some name is, in terms of parent scopes; we can then offer closer names first 401 | while (node) { 402 | if (node.containedScope) { 403 | for (const searchScope of ["local", "arguments", "variables"] as StaticallyKnownScopeName[]) { 404 | const symTab = node.containedScope[searchScope]; 405 | if (!symTab) continue; 406 | 407 | const iterableSymTabEntries = [...symTab.values()]; 408 | if (node.containedScope.typeinfo.mergedInterfaces.has(searchScope)) { 409 | iterableSymTabEntries.push(...node.containedScope.typeinfo.mergedInterfaces.get(searchScope)!.members.values()); 410 | } 411 | 412 | for (const symTabEntry of iterableSymTabEntries) { 413 | const completionKind = symTabEntry.firstLexicalType?.kind === TypeKind.functionSignature 414 | ? CompletionItemKind.function 415 | : CompletionItemKind.variable; 416 | result.set(symTabEntry.uiName, [searchScope, completionKind, scopeDistance]); 417 | } 418 | } 419 | scopeDistance++; 420 | } 421 | 422 | // if we're at the root of a sourcefile, try to climb into the parent CFC if it exists 423 | // otherwise, climb to ancestor container or direct parent node 424 | node = (node.kind === NodeKind.sourceFile && node.cfc?.extends) 425 | ? node.cfc.extends 426 | : node.containedScope?.parentContainer || node.parent; 427 | } 428 | return result; 429 | })(node); 430 | 431 | for (const [varName, [_, completionKind, scopeDistance]] of allVisibleNames) { 432 | result.push({ 433 | label: varName, 434 | kind: completionKind, 435 | // sort the first two scopes to the top of the list; the rest get lexically sorted as one agglomerated scope 436 | sortText: (scopeDistance === 0 ? 'a' : scopeDistance === 1 ? 'b' : 'c') + scopeDistance 437 | }); 438 | } 439 | 440 | return result; 441 | } --------------------------------------------------------------------------------