diff --git a/gulpfile.js b/gulpfile.js index 8b9cbb125349..558e83ff013f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -61,6 +61,8 @@ gulp.task('watch', ['hygiene-modified', 'hygiene-watch']); gulp.task('hygiene-watch', () => gulp.watch(all, debounce(() => run({ mode: 'changes' }), 1000))); +gulp.task('hygiene-all', () => run({ mode: 'all' })); + gulp.task('hygiene-modified', ['compile'], () => run({ mode: 'changes' })); gulp.task('clean', ['output:clean', 'cover:clean'], () => { }); diff --git a/src/client/common/net/socket/socketCallbackHandler.ts b/src/client/common/net/socket/socketCallbackHandler.ts index 3f95f3499067..dda2a4d4cf92 100644 --- a/src/client/common/net/socket/socketCallbackHandler.ts +++ b/src/client/common/net/socket/socketCallbackHandler.ts @@ -1,3 +1,5 @@ +// tslint:disable:quotemark ordered-imports member-ordering one-line prefer-const + "use strict"; import * as net from "net"; @@ -6,7 +8,7 @@ import { SocketStream } from "./SocketStream"; import { SocketServer } from './socketServer'; export abstract class SocketCallbackHandler extends EventEmitter { - private _stream: SocketStream = null; + private _stream: SocketStream; private commandHandlers: Map; private handeshakeDone: boolean; @@ -41,8 +43,8 @@ export abstract class SocketCallbackHandler extends EventEmitter { protected abstract handleHandshake(): boolean; - private HandleIncomingData(buffer: Buffer, socket: net.Socket): boolean { - if (this._stream === null) { + private HandleIncomingData(buffer: Buffer, socket: net.Socket): boolean | undefined { + if (!this._stream) { this._stream = new SocketStream(socket, buffer); } else { @@ -72,7 +74,7 @@ export abstract class SocketCallbackHandler extends EventEmitter { } if (this.commandHandlers.has(cmd)) { - const handler = this.commandHandlers.get(cmd); + const handler = this.commandHandlers.get(cmd)!; handler(); } else { diff --git a/src/client/debugger/Common/Contracts.ts b/src/client/debugger/Common/Contracts.ts index 15fbc9b9dee4..88396172cb69 100644 --- a/src/client/debugger/Common/Contracts.ts +++ b/src/client/debugger/Common/Contracts.ts @@ -5,6 +5,7 @@ import { ChildProcess } from 'child_process'; import * as net from 'net'; import { OutputEvent } from 'vscode-debugadapter'; import { DebugProtocol } from 'vscode-debugprotocol'; +import { DebuggerPerformanceTelemetry, DebuggerTelemetry } from '../../telemetry/types'; export class TelemetryEvent extends OutputEvent { body: { @@ -15,7 +16,7 @@ export class TelemetryEvent extends OutputEvent { /** Optional data to report. For the 'telemetry' category the data will be sent to telemetry, for the other categories the data is shown in JSON format. */ data?: any; }; - constructor(output: string, data?: any) { + constructor(output: string, data?: DebuggerTelemetry | DebuggerPerformanceTelemetry) { super(output, 'telemetry'); if (data) { this.body.data = data; @@ -139,7 +140,7 @@ export interface IPythonEvaluationResult { ExceptionText?: string; Expression: string; ChildName: string; - Process: IPythonProcess; + Process?: IPythonProcess; Frame: IPythonStackFrame; } diff --git a/src/client/debugger/Common/Terminal.ts b/src/client/debugger/Common/Terminal.ts deleted file mode 100644 index 1c2308ea442f..000000000000 --- a/src/client/debugger/Common/Terminal.ts +++ /dev/null @@ -1,286 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as FS from 'fs'; -import * as CP from 'child_process'; - -export class Terminal -{ - private static _terminalService: ITerminalService; - - public static launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - return this.terminalService().launchInTerminal(dir, args, envVars); - } - - public static killTree(processId: number): void { - this.terminalService().killTree(processId); - } - - /* - * Is the given runtime executable on the PATH. - */ - public static isOnPath(program: string): boolean { - return this.terminalService().isOnPath(program); - } - - private static terminalService(): ITerminalService { - if (!this._terminalService) { - if (process.platform === 'win32') { - this._terminalService = new WindowsTerminalService(); - } else if (process.platform === 'darwin') { - this._terminalService = new MacTerminalService(); - } else if (process.platform === 'linux') { - this._terminalService = new LinuxTerminalService(); - } else { - this._terminalService = new DefaultTerminalService(); - } - } - return this._terminalService; - } -} - - -export class TerminalError { - - public message: string; - public linkId: number; - - constructor(message: string, linkId?: number) { - this.message = message; - this.linkId = linkId; - } -} - -interface ITerminalService { - launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise; - killTree(pid: number): void; - isOnPath(program: string): boolean; -} - -class DefaultTerminalService implements ITerminalService { - - protected static TERMINAL_TITLE = "Python Console"; - private static WHICH = '/usr/bin/which'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - return new Promise( (resolve, reject) => { - reject(new TerminalError(`External console not implemented on '${process.platform}'.`)); - }); - } - - public killTree(pid: number): void { - - // on linux and OS X we kill all direct and indirect child processes as well - try { - const cmd = Path.join(__dirname, './terminateProcess.sh'); - CP.spawnSync(cmd, [ pid.toString() ]); - } catch (err) { - } - } - - public isOnPath(program: string): boolean { - - try { - if (FS.existsSync(DefaultTerminalService.WHICH)) { - CP.execSync(`${DefaultTerminalService.WHICH} '${program}'`); - } else { - // do not report error if 'which' doesn't exist - } - return true; - } - catch (Exception) { - } - return false; - } -} - -class WindowsTerminalService extends DefaultTerminalService { - - private static CMD = 'cmd.exe'; - private static WHERE = 'C:\\Windows\\System32\\where.exe'; - private static TASK_KILL = 'C:\\Windows\\System32\\taskkill.exe'; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - const title = `"${dir} - ${WindowsTerminalService.TERMINAL_TITLE}"`; - const command = `""${args.join('" "')}" & pause"`; // use '|' to only pause on non-zero exit code - - const cmdArgs = [ - '/c', 'start', title, '/wait', - 'cmd.exe', '/c', command - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject( { }, process.env), envVars); - - const options: any = { - cwd: dir, - env: env, - windowsVerbatimArguments: true - }; - - const cmd = CP.spawn(WindowsTerminalService.CMD, cmdArgs, options); - cmd.on('error', reject); - - resolve(cmd); - }); - } - - public killTree(pid: number): void { - - // when killing a process in Windows its child processes are *not* killed but become root processes. - // Therefore we use TASKKILL.EXE - - try { - CP.execSync(`${WindowsTerminalService.TASK_KILL} /F /T /PID ${pid}`); - } - catch (err) { - } - } - - public isOnPath(program: string): boolean { - - try { - if (FS.existsSync(WindowsTerminalService.WHERE)) { - CP.execSync(`${WindowsTerminalService.WHERE} ${program}`); - } else { - // do not report error if 'where' doesn't exist - } - return true; - } - catch (Exception) { - // ignore - } - return false; - } -} - -class LinuxTerminalService extends DefaultTerminalService { - - private static LINUX_TERM = '/usr/bin/gnome-terminal'; //private const string LINUX_TERM = "/usr/bin/x-terminal-emulator"; - private static WAIT_MESSAGE = "Press any key to continue..."; - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - if (!FS.existsSync(LinuxTerminalService.LINUX_TERM)) { - reject(new TerminalError(`'${LinuxTerminalService.LINUX_TERM}' not found`, 20002)); - return; - } - - const bashCommand = `${quote(args)}; echo; read -p "${LinuxTerminalService.WAIT_MESSAGE}" -n1;`; - - const termArgs = [ - '--title', `"${LinuxTerminalService.TERMINAL_TITLE}"`, - '-x', 'bash', '-c', - `''${bashCommand}''` // wrapping argument in two sets of ' because node is so "friendly" that it removes one set... - ]; - - // merge environment variables into a copy of the process.env - const env = extendObject(extendObject( { }, process.env), envVars); - - const options: any = { - cwd: dir, - env: env - }; - - const cmd = CP.spawn(LinuxTerminalService.LINUX_TERM, termArgs, options); - cmd.on('error', reject); - cmd.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just a launcher, we do not pass it in the resolve to the caller - } else { - reject(new TerminalError(`${LinuxTerminalService.LINUX_TERM} failed with exit code ${code}`)); - } - }); - }); - } -} - -class MacTerminalService extends DefaultTerminalService { - - private static OSASCRIPT = '/usr/bin/osascript'; // osascript is the AppleScript interpreter on OS X - - public launchInTerminal(dir: string, args: string[], envVars: { [key: string]: string; }): Promise { - - return new Promise( (resolve, reject) => { - - // first fix the PATH so that 'runtimePath' can be found if installed with 'brew' - // Utilities.FixPathOnOSX(); - - // On OS X we do not launch the program directly but we launch an AppleScript that creates (or reuses) a Terminal window - // and then launches the program inside that window. - - const osaArgs = [ - Path.join(__dirname, './TerminalHelper.scpt'), - '-t', MacTerminalService.TERMINAL_TITLE, - '-w', dir, - ]; - - for (let a of args) { - osaArgs.push('-pa'); - osaArgs.push(a); - } - - if (envVars) { - for (let key in envVars) { - osaArgs.push('-e'); - osaArgs.push(key + '=' + envVars[key]); - } - } - - let stderr = ''; - const osa = CP.spawn(MacTerminalService.OSASCRIPT, osaArgs); - osa.on('error', reject); - osa.stderr.on('data', (data) => { - stderr += data.toString(); - }); - osa.on('exit', (code: number) => { - if (code === 0) { // OK - resolve(); // since cmd is not the terminal process but just the osa tool, we do not pass it in the resolve to the caller - } else { - if (stderr) { - reject(new TerminalError(stderr)); - } else { - reject(new TerminalError(`{MacTerminalService.OSASCRIPT} failed with exit code ${code}`)); - } - } - }); - }); - } -} - -// ---- private utilities ---- - -/** - * Quote args if necessary and combine into a space separated string. - */ -function quote(args: string[]): string { - let r = ''; - for (let a of args) { - if (a.indexOf(' ') >= 0) { - r += '"' + a + '"'; - } else { - r += a; - } - r += ' '; - } - return r; -} - -function extendObject (objectCopy: T, object: T): T { - - for (let key in object) { - if (object.hasOwnProperty(key)) { - objectCopy[key] = object[key]; - } - } - - return objectCopy; -} \ No newline at end of file diff --git a/src/client/debugger/Common/TryParser.ts b/src/client/debugger/Common/TryParser.ts index 473a7d9b23ba..b3b2482b5319 100644 --- a/src/client/debugger/Common/TryParser.ts +++ b/src/client/debugger/Common/TryParser.ts @@ -1,6 +1,7 @@ +// tslint:disable:quotemark no-var-requires no-require-imports max-func-body-length prefer-const no-function-expression cyclomatic-complexity no-increment-decrement one-line + "use strict"; -import * as path from "path"; const LineByLineReader = require("line-by-line"); export interface ITryStatement { @@ -42,7 +43,7 @@ export function ExtractTryStatements(pythonFile: string): Promise 0 && (trimmedLine.startsWith("else ") || trimmedLine.startsWith("else:"))) { - // This is possibly an if else... + // This is possibly an if else... if (tryColumnBlocks.size === 0) { return; } @@ -102,7 +103,7 @@ export function ExtractTryStatements(pythonFile: string): Promise { ex = ex.trim(); if (ex.length === 0) { @@ -180,4 +181,4 @@ function extractExceptions(line: string): string[] { }); return exceptions; -} \ No newline at end of file +} diff --git a/src/client/debugger/Common/Utils.ts b/src/client/debugger/Common/Utils.ts index 621eb91f4977..6ce926fcd6c5 100644 --- a/src/client/debugger/Common/Utils.ts +++ b/src/client/debugger/Common/Utils.ts @@ -1,3 +1,4 @@ +// tslint:disable:quotemark no-var-requires no-require-imports max-func-body-length prefer-const no-function-expression cyclomatic-complexity no-increment-decrement one-line 'use strict'; import * as child_process from 'child_process'; diff --git a/src/client/debugger/Common/pathUtilities.ts b/src/client/debugger/Common/pathUtilities.ts deleted file mode 100644 index f63994434648..000000000000 --- a/src/client/debugger/Common/pathUtilities.ts +++ /dev/null @@ -1,204 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as Path from 'path'; -import * as FS from 'fs'; - - -/** - * The input paths must use the path syntax of the underlying operating system. - */ -export function makePathAbsolute(absPath: string, relPath: string): string { - return Path.resolve(Path.dirname(absPath), relPath); -} - -/** - * Remove the first segment of the given path and return the result. - * The input path must use the path syntax of the underlying operating system. - */ -export function removeFirstSegment(path: string): string { - const segments = path.split(Path.sep); - segments.shift(); - if (segments.length > 0) { - return segments.join(Path.sep); - } - return null; -} - -/** - * Return the relative path between 'path' and 'target'. - * The input paths must use the path syntax of the underlying operating system. - */ -export function makeRelative(target: string, path: string): string { - const t = target.split(Path.sep); - const p = path.split(Path.sep); - - let i = 0; - for (; i < Math.min(t.length, p.length) && t[i] === p[i]; i++) { - } - - let result = ''; - for (; i < p.length; i++) { - result = Path.join(result, p[i]); - } - return result; -} - -/** - * Returns a path with a lower case drive letter. - */ -export function normalizeDriveLetter(path: string): string { - const regex = /^([A-Z])(\:[\\\/].*)$/; - if (regex.test(path)) { - path = path.replace(regex, (s, s1, s2) => s1.toLowerCase() + s2); - } - return path; -} - -export function pathCompare(path1: string, path2: string): boolean { - return normalizeDriveLetter(path1) === normalizeDriveLetter(path2); -} - -/** - * Given an absolute, normalized, and existing file path 'realPath' returns the exact path that the file has on disk. - * On a case insensitive file system, the returned path might differ from the original path by character casing. - * On a case sensitive file system, the returned path will always be identical to the original path. - * In case of errors, null is returned. But you cannot use this function to verify that a path exists. - * realPath does not handle '..' or '.' path segments and it does not take the locale into account. - * Since a drive letter of a Windows path cannot be looked up, realPath normalizes the drive letter to lower case. - */ -export function realPath(path: string): string { - - let dir = Path.dirname(path); - if (path === dir) { // end recursion - // is this an upper case drive letter? - if (/^[A-Z]\:\\$/.test(path)) { - path = path.toLowerCase(); - } - return path; - } - let name = Path.basename(path).toLowerCase(); - try { - let entries = FS.readdirSync(dir); - let found = entries.filter(e => e.toLowerCase() === name); // use a case insensitive search - if (found.length === 1) { - // on a case sensitive filesystem we cannot determine here, whether the file exists or not, hence we need the 'file exists' precondition - let prefix = realPath(dir); // recurse - if (prefix) { - return Path.join(prefix, found[0]); - } - } else if (found.length > 1) { - // must be a case sensitive $filesystem - const ix = found.indexOf(name); - if (ix >= 0) { // case sensitive - let prefix = realPath(dir); // recurse - if (prefix) { - return Path.join(prefix, found[ix]); - } - } - } - } - catch (error) { - // silently ignore error - } - return null; -} - -/** - * Make sure that all directories of the given path exist (like mkdir -p). - */ -export function mkdirs(path: string) { - if (!FS.existsSync(path)) { - mkdirs(Path.dirname(path)); - FS.mkdirSync(path); - } -} - -//---- the following functions work with Windows and Unix-style paths independent from the underlying OS. - -/** - * Returns true if the Windows or Unix-style path is absolute. - */ -export function isAbsolutePath(path: string) { - if (path) { - if (path.charAt(0) === '/') { - return true; - } - if (/^[a-zA-Z]\:[\\\/]/.test(path)) { - return true; - } - } - return false; -} - -/** - * Convert the given Windows or Unix-style path into a normalized path that only uses forward slashes and has all superflous '..' sequences removed. - * If the path starts with a Windows-style drive letter, a '/' is prepended. - */ -export function normalize(path: string) : string { - - path = path.replace(/\\/g, '/'); - if (/^[a-zA-Z]\:\//.test(path)) { - path = '/' + path; - } - path = Path.normalize(path); // use node's normalize to remove '/..' etc. - path = path.replace(/\\/g, '/'); - return path; -} - -/** - * Convert the given normalized path into a Windows-style path. - */ -export function toWindows(path: string) : string { - if (/^\/[a-zA-Z]\:\//.test(path)) { - path = path.substr(1); - } - path = path.replace(/\//g, '\\'); - return path; -} - -/** - * Append the given relative path to the absolute path and normalize the result. - */ -export function join(absPath: string, relPath: string) : string { - absPath = normalize(absPath); - relPath = normalize(relPath); - if (absPath.charAt(absPath.length-1) === '/') { - absPath = absPath + relPath; - } else { - absPath = absPath + '/' + relPath; - } - absPath = Path.normalize(absPath); - absPath = absPath.replace(/\\/g, '/'); - return absPath; -} - -/** - * Return the relative path between 'from' and 'to'. - */ -export function makeRelative2(from: string, to: string): string { - - from = normalize(from); - to = normalize(to); - - const froms = from.substr(1).split('/'); - const tos = to.substr(1).split('/'); - - while (froms.length > 0 && tos.length > 0 && froms[0] === tos[0]) { - froms.shift(); - tos.shift(); - } - - let l = froms.length - tos.length; - if (l === 0) { - l = tos.length - 1; - } - - while (l > 0) { - tos.unshift('..'); - l--; - } - return tos.join('/'); -} \ No newline at end of file diff --git a/src/client/debugger/Common/telemetry.ts b/src/client/debugger/Common/telemetry.ts new file mode 100644 index 000000000000..cecde60b2658 --- /dev/null +++ b/src/client/debugger/Common/telemetry.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-function-expression no-any no-invalid-this no-use-before-declare + +import { DebugSession, StoppedEvent } from 'vscode-debugadapter'; +import { DEBUGGER_PERFORMANCE } from '../../telemetry/constants'; +import { StopWatch } from '../../telemetry/stopWatch'; +import { DebuggerPerformanceTelemetry } from '../../telemetry/types'; +import { TelemetryEvent } from './Contracts'; + +type DebugAction = 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; +type DebugPerformanceInformation = { action: DebugAction, timer: StopWatch }; + +const executionStack: DebugPerformanceInformation[] = []; + +export enum PerformanceTelemetryCondition { + always = 0, + stoppedEvent = 1 +} + +export function capturePerformanceTelemetry(action: DebugAction) { + return function (target: DebugSession, _propertyKey: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + executionStack.push({ action, timer: new StopWatch() }); + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} + +export function sendPerformanceTelemetry(condition: PerformanceTelemetryCondition) { + return function (target: DebugSession, propertyKey: string, descriptor: TypedPropertyDescriptor) { + const originalMethod = descriptor.value; + descriptor.value = function (...args: any[]) { + if (propertyKey === 'sendEvent' && args.length === 1 && args[0] instanceof TelemetryEvent) { + return originalMethod.apply(this, args); + } + + try { + const data = getPerformanceTelemetryData(condition, args); + if (data) { + this.sendEvent(new TelemetryEvent(DEBUGGER_PERFORMANCE, data)); + } + } catch { + // We don't want errors here interfering the user's work, hence swallow exceptions. + } + return originalMethod.apply(this, args); + }; + + return descriptor; + }; +} + +function getPerformanceTelemetryData(condition: PerformanceTelemetryCondition, functionArgs: any[]): DebuggerPerformanceTelemetry | undefined { + if (executionStack.length === 0) { + return; + } + let item: DebugPerformanceInformation | undefined; + switch (condition) { + case PerformanceTelemetryCondition.always: { + item = executionStack.pop(); + } + case PerformanceTelemetryCondition.stoppedEvent: { + if (functionArgs.length > 0 && functionArgs[0] instanceof StoppedEvent) { + item = executionStack.pop(); + } + break; + } + default: { + return; + } + } + if (item) { + return { action: item!.action, duration: item!.timer.elapsedTime }; + } +} diff --git a/src/client/debugger/DebugClients/DebugClient.ts b/src/client/debugger/DebugClients/DebugClient.ts index c96eca5e34a6..2c3c9d99f77a 100644 --- a/src/client/debugger/DebugClients/DebugClient.ts +++ b/src/client/debugger/DebugClients/DebugClient.ts @@ -1,11 +1,9 @@ -import {BaseDebugServer} from "../DebugServers/BaseDebugServer"; -import {LocalDebugServer} from "../DebugServers/LocalDebugServer"; -import {IPythonProcess, IPythonThread, IDebugServer} from "../Common/Contracts"; -import {DebugSession, OutputEvent} from "vscode-debugadapter"; -import * as path from "path"; -import * as child_process from "child_process"; -import {DjangoApp, LaunchRequestArguments, AttachRequestArguments} from "../Common/Contracts"; -import {EventEmitter} from 'events'; +// tslint:disable:quotemark ordered-imports no-any no-empty + +import { BaseDebugServer } from "../DebugServers/BaseDebugServer"; +import { IPythonProcess, IDebugServer } from "../Common/Contracts"; +import { DebugSession } from "vscode-debugadapter"; +import { EventEmitter } from 'events'; export enum DebugType { Local, diff --git a/src/client/debugger/DebugClients/DebugFactory.ts b/src/client/debugger/DebugClients/DebugFactory.ts index fd17f4151a97..7824ec9b5cd4 100644 --- a/src/client/debugger/DebugClients/DebugFactory.ts +++ b/src/client/debugger/DebugClients/DebugFactory.ts @@ -1,10 +1,5 @@ -import * as child_process from 'child_process'; -import * as path from 'path'; -import { DebugSession, OutputEvent } from 'vscode-debugadapter'; -import { IDebugServer, IPythonProcess, IPythonThread } from '../Common/Contracts'; -import { AttachRequestArguments, DjangoApp, LaunchRequestArguments } from '../Common/Contracts'; -import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; -import { LocalDebugServer } from '../DebugServers/LocalDebugServer'; +import { DebugSession } from 'vscode-debugadapter'; +import { AttachRequestArguments, LaunchRequestArguments } from '../Common/Contracts'; import { DebugClient } from './DebugClient'; import { LocalDebugClient } from './LocalDebugClient'; import { NonDebugClient } from './NonDebugClient'; diff --git a/src/client/debugger/DebugClients/RemoteDebugClient.ts b/src/client/debugger/DebugClients/RemoteDebugClient.ts index 88cb9480fb9f..1acccaadbf60 100644 --- a/src/client/debugger/DebugClients/RemoteDebugClient.ts +++ b/src/client/debugger/DebugClients/RemoteDebugClient.ts @@ -1,13 +1,12 @@ import { DebugSession } from 'vscode-debugadapter'; import { IPythonProcess } from '../Common/Contracts'; -import { AttachRequestArguments } from '../Common/Contracts'; import { BaseDebugServer } from '../DebugServers/BaseDebugServer'; import { RemoteDebugServer } from '../DebugServers/RemoteDebugServer'; import { DebugClient, DebugType } from './DebugClient'; export class RemoteDebugClient extends DebugClient { private pythonProcess: IPythonProcess; - private debugServer: BaseDebugServer; + private debugServer?: BaseDebugServer; // tslint:disable-next-line:no-any constructor(args: any, debugSession: DebugSession) { super(args, debugSession); @@ -16,7 +15,7 @@ export class RemoteDebugClient extends DebugClient { public CreateDebugServer(pythonProcess: IPythonProcess): BaseDebugServer { this.pythonProcess = pythonProcess; this.debugServer = new RemoteDebugServer(this.debugSession, this.pythonProcess, this.args); - return this.debugServer; + return this.debugServer!; } public get DebugType(): DebugType { return DebugType.Remote; @@ -28,7 +27,7 @@ export class RemoteDebugClient extends DebugClient { } if (this.debugServer) { this.debugServer.Stop(); - this.debugServer = null; + this.debugServer = undefined; } } diff --git a/src/client/debugger/DebugServers/BaseDebugServer.ts b/src/client/debugger/DebugServers/BaseDebugServer.ts index 0513b5c07223..824c6f103299 100644 --- a/src/client/debugger/DebugServers/BaseDebugServer.ts +++ b/src/client/debugger/DebugServers/BaseDebugServer.ts @@ -1,9 +1,10 @@ +// tslint:disable:quotemark ordered-imports no-any no-empty "use strict"; -import {DebugSession} from "vscode-debugadapter"; -import {IPythonProcess, IDebugServer} from "../Common/Contracts"; -import {EventEmitter} from "events"; -import {Deferred, createDeferred} from '../../common/helpers'; +import { DebugSession } from "vscode-debugadapter"; +import { IPythonProcess, IDebugServer } from "../Common/Contracts"; +import { EventEmitter } from "events"; +import { Deferred, createDeferred } from '../../common/helpers'; export abstract class BaseDebugServer extends EventEmitter { protected pythonProcess: IPythonProcess; @@ -26,4 +27,4 @@ export abstract class BaseDebugServer extends EventEmitter { public abstract Start(): Promise; public abstract Stop(); -} \ No newline at end of file +} diff --git a/src/client/debugger/DebugServers/RemoteDebugServer.ts b/src/client/debugger/DebugServers/RemoteDebugServer.ts index 2899d103def4..32d93d004920 100644 --- a/src/client/debugger/DebugServers/RemoteDebugServer.ts +++ b/src/client/debugger/DebugServers/RemoteDebugServer.ts @@ -1,22 +1,20 @@ +// tslint:disable:quotemark ordered-imports no-any no-empty curly member-ordering one-line max-func-body-length no-var-self prefer-const cyclomatic-complexity prefer-template "use strict"; -import {DebugSession, OutputEvent} from "vscode-debugadapter"; -import {IPythonProcess, IDebugServer, AttachRequestArguments} from "../Common/Contracts"; +import { DebugSession, OutputEvent } from "vscode-debugadapter"; +import { IPythonProcess, IDebugServer, AttachRequestArguments } from "../Common/Contracts"; import * as net from "net"; -import {BaseDebugServer} from "./BaseDebugServer"; -import {SocketStream} from "../../common/net/socket/SocketStream"; +import { BaseDebugServer } from "./BaseDebugServer"; +import { SocketStream } from "../../common/net/socket/SocketStream"; const DebuggerProtocolVersion = 6; // must be kept in sync with PTVSDBG_VER in attach_server.py const DebuggerSignature = "PTVSDBG"; const Accepted = "ACPT"; -const Rejected = "RJCT"; const DebuggerSignatureBytes: Buffer = new Buffer(DebuggerSignature, "ascii"); -const InfoCommandBytes: Buffer = new Buffer("INFO", "ascii"); const AttachCommandBytes: Buffer = new Buffer("ATCH", "ascii"); -const ReplCommandBytes: Buffer = new Buffer("REPL", "ascii"); export class RemoteDebugServer extends BaseDebugServer { - private socket: net.Socket = null; + private socket?: net.Socket; private args: AttachRequestArguments; constructor(debugSession: DebugSession, pythonProcess: IPythonProcess, args: AttachRequestArguments) { super(debugSession, pythonProcess); @@ -24,50 +22,48 @@ export class RemoteDebugServer extends BaseDebugServer { } public Stop() { - if (this.socket === null) return; + if (!this.socket) return; try { this.socket.end(); } catch (ex) { } - this.socket = null; + this.socket = undefined; } - private stream: SocketStream = null; + private stream: SocketStream; public Start(): Promise { return new Promise((resolve, reject) => { let that = this; let connected = false; let secretWrittenToDebugProgram = false; let secretConfirmedByDebugProgram = false; - let infoBytesWritten = false; - let versionRead = false; let commandBytesWritten = false; let languageVersionRead = false; let portNumber = this.args.port; let debugCommandsAccepted = false; - let options = { port: portNumber}; + let options = { port: portNumber! }; if (typeof this.args.host === "string" && this.args.host.length > 0) { (options).host = this.args.host; } this.socket = net.connect(options, () => { resolve(options); }); - this.socket.on("end", (ex) => { + this.socket!.on("end", (ex) => { // let msg = `Debugger client disconneced, ex`; // that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); }); - this.socket.on("data", (buffer: Buffer) => { + this.socket!.on("data", (buffer: Buffer) => { if (connected) { that.pythonProcess.HandleIncomingData(buffer); return; } - if (that.stream === null) { - that.stream = new SocketStream(that.socket, buffer); + if (!that.stream) { + that.stream = new SocketStream(that.socket!, buffer); } else { if (!connected) { if (that.stream.Length === 0) { - that.stream = new SocketStream(that.socket, buffer); + that.stream = new SocketStream(that.socket!, buffer); } else { that.stream.Append(buffer); @@ -102,7 +98,6 @@ export class RemoteDebugServer extends BaseDebugServer { throw new Error("ConnErrorMessages.RemoteUnsupportedServer"); } - that.stream.WriteString(that.args.secret || ""); secretWrittenToDebugProgram = true; that.stream.EndTransaction(); @@ -157,11 +152,10 @@ export class RemoteDebugServer extends BaseDebugServer { if (debugCommandsAccepted && !languageVersionRead) { that.stream.EndTransaction(); - let pid = that.stream.ReadInt32(); - let langMajor = that.stream.ReadInt32(); - let langMinor = that.stream.ReadInt32(); - let langMicro = that.stream.ReadInt32(); - let langVer = ((langMajor << 8) | langMinor); + that.stream.ReadInt32(); + that.stream.ReadInt32(); + that.stream.ReadInt32(); + that.stream.ReadInt32(); if (that.stream.HasInsufficientDataForReading) { that.stream.RollBackTransaction(); return; @@ -176,21 +170,20 @@ export class RemoteDebugServer extends BaseDebugServer { that.pythonProcess.HandleIncomingData(buffer); } else { - that.pythonProcess.Connect(that.stream.Buffer, this.socket, true); + that.pythonProcess.Connect(that.stream.Buffer, this.socket!, true); connected = true; } } }); - this.socket.on("close", d => { - let msg = `Debugger client closed, ${d}`; + this.socket!.on("close", d => { that.emit("detach", d); }); - this.socket.on("timeout", d => { + this.socket!.on("timeout", d => { let msg = `Debugger client timedout, ${d}`; that.debugSession.sendEvent(new OutputEvent(msg + "\n", "stderr")); }); - this.socket.on("error", ex => { - if (connected){ + this.socket!.on("error", ex => { + if (connected) { return; } let exMessage = JSON.stringify(ex); diff --git a/src/client/debugger/Main.ts b/src/client/debugger/Main.ts index d7bb501d8df2..d7d848eb79fd 100644 --- a/src/client/debugger/Main.ts +++ b/src/client/debugger/Main.ts @@ -1,3 +1,4 @@ +// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self "use strict"; // This line should always be right on top. @@ -8,7 +9,7 @@ if ((Reflect as any).metadata === undefined) { } import * as fs from "fs"; import * as path from "path"; -import { DebugSession, Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread } from "vscode-debugadapter"; +import { DebugSession, Handles, InitializedEvent, OutputEvent, Scope, Source, StackFrame, StoppedEvent, TerminatedEvent, Thread, Variable } from "vscode-debugadapter"; import { ThreadEvent } from "vscode-debugadapter"; import { DebugProtocol } from "vscode-debugprotocol"; import { DEBUGGER } from '../../client/telemetry/constants'; @@ -23,6 +24,7 @@ import { CreateAttachDebugClient, CreateLaunchDebugClient } from "./DebugClients import { BaseDebugServer } from "./DebugServers/BaseDebugServer"; import { PythonProcess } from "./PythonProcess"; import { IS_WINDOWS } from './Common/Utils'; +import { sendPerformanceTelemetry, capturePerformanceTelemetry, PerformanceTelemetryCondition } from "./Common/telemetry"; const CHILD_ENUMEARATION_TIMEOUT = 5000; @@ -39,10 +41,10 @@ export class PythonDebugger extends DebugSession { private registeredBreakpointsByFileName: Map; private debuggerLoaded: Promise; private debuggerLoadedPromiseResolve: () => void; - private debugClient: DebugClient; + private debugClient?: DebugClient; private configurationDone: Promise; - private configurationDonePromiseResolve: () => void; - private lastException: IPythonException; + private configurationDonePromiseResolve?: () => void; + private lastException?: IPythonException; private _supportsRunInTerminalRequest: boolean; private terminateEventSent: boolean; public constructor(debuggerLinesStartAt1: boolean, isServer: boolean) { @@ -55,14 +57,19 @@ export class PythonDebugger extends DebugSession { this.debuggerLoadedPromiseResolve = resolve; }); } + // tslint:disable-next-line:no-unnecessary-override + @sendPerformanceTelemetry(PerformanceTelemetryCondition.stoppedEvent) + public sendEvent(event: DebugProtocol.Event): void { + super.sendEvent(event); + } protected initializeRequest(response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void { - response.body.supportsEvaluateForHovers = true; - response.body.supportsConditionalBreakpoints = true; - response.body.supportsConfigurationDoneRequest = true; - response.body.supportsEvaluateForHovers = false; - response.body.supportsFunctionBreakpoints = false; - response.body.supportsSetVariable = true; - response.body.exceptionBreakpointFilters = [ + response.body!.supportsEvaluateForHovers = true; + response.body!.supportsConditionalBreakpoints = true; + response.body!.supportsConfigurationDoneRequest = true; + response.body!.supportsEvaluateForHovers = false; + response.body!.supportsFunctionBreakpoints = false; + response.body!.supportsSetVariable = true; + response.body!.exceptionBreakpointFilters = [ { label: "All Exceptions", filter: "all" @@ -80,7 +87,7 @@ export class PythonDebugger extends DebugSession { this.sendEvent(new InitializedEvent()); } - private pythonProcess: PythonProcess; + private pythonProcess?: PythonProcess; private debugServer: BaseDebugServer; private startDebugServer(): Promise { @@ -92,35 +99,36 @@ export class PythonDebugger extends DebugSession { programDirectory = this.launchArgs.cwd; } this.pythonProcess = new PythonProcess(0, "", programDirectory); - this.debugServer = this.debugClient.CreateDebugServer(this.pythonProcess); + this.debugServer = this.debugClient!.CreateDebugServer(this.pythonProcess!); this.InitializeEventHandlers(); return this.debugServer.Start(); } private stopDebugServer() { if (this.debugClient) { - this.debugClient.Stop(); - this.debugClient = null; + this.debugClient!.Stop(); + this.debugClient = undefined; } if (this.pythonProcess) { - this.pythonProcess.Kill(); - this.pythonProcess = null; + this.pythonProcess!.Kill(); + this.pythonProcess = undefined; } this.terminateEventSent = true; this.sendEvent(new TerminatedEvent()); } private InitializeEventHandlers() { - this.pythonProcess.on("last", arg => this.onLastCommand()); - this.pythonProcess.on("threadExited", arg => this.onPythonThreadExited(arg)); - this.pythonProcess.on("moduleLoaded", arg => this.onPythonModuleLoaded(arg)); - this.pythonProcess.on("threadCreated", arg => this.onPythonThreadCreated(arg)); - this.pythonProcess.on("processLoaded", arg => this.onPythonProcessLoaded(arg)); - this.pythonProcess.on("output", (pyThread, output) => this.onDebuggerOutput(pyThread, output, 'stdout')); - this.pythonProcess.on("exceptionRaised", (pyThread, ex) => this.onPythonException(pyThread, ex)); - this.pythonProcess.on("breakpointHit", (pyThread, breakpointId) => this.onBreakpointHit(pyThread, breakpointId)); - this.pythonProcess.on("stepCompleted", (pyThread) => this.onStepCompleted(pyThread)); - this.pythonProcess.on("detach", () => this.onDetachDebugger()); - this.pythonProcess.on("error", ex => this.onDebuggerOutput(undefined, ex, 'stderr')); - this.pythonProcess.on("asyncBreakCompleted", arg => this.onPythonProcessPaused(arg)); + const pythonProcess = this.pythonProcess!; + pythonProcess.on("last", arg => this.onLastCommand()); + pythonProcess.on("threadExited", arg => this.onPythonThreadExited(arg)); + pythonProcess.on("moduleLoaded", arg => this.onPythonModuleLoaded(arg)); + pythonProcess.on("threadCreated", arg => this.onPythonThreadCreated(arg)); + pythonProcess.on("processLoaded", arg => this.onPythonProcessLoaded(arg)); + pythonProcess.on("output", (pyThread, output) => this.onDebuggerOutput(pyThread, output, 'stdout')); + pythonProcess.on("exceptionRaised", (pyThread, ex) => this.onPythonException(pyThread, ex)); + pythonProcess.on("breakpointHit", (pyThread, breakpointId) => this.onBreakpointHit(pyThread, breakpointId)); + pythonProcess.on("stepCompleted", (pyThread) => this.onStepCompleted(pyThread)); + pythonProcess.on("detach", () => this.onDetachDebugger()); + pythonProcess.on("error", ex => this.onDebuggerOutput(undefined, ex, 'stderr')); + pythonProcess.on("asyncBreakCompleted", arg => this.onPythonProcessPaused(arg)); this.debugServer.on("detach", () => this.onDetachDebugger()); } @@ -154,9 +162,8 @@ export class PythonDebugger extends DebugSession { } private onPythonModuleLoaded(module: IPythonModule) { } - private debuggerHasLoaded: boolean; + @sendPerformanceTelemetry(PerformanceTelemetryCondition.always) private onPythonProcessLoaded(pyThread?: IPythonThread) { - this.debuggerHasLoaded = true; if (this.entryResponse) { this.sendResponse(this.entryResponse); } @@ -176,10 +183,10 @@ export class PythonDebugger extends DebugSession { this.sendEvent(new StoppedEvent("entry", thread.Id)); } else if (this.launchArgs && this.launchArgs.stopOnEntry === false) { this.configurationDone.then(() => { - this.pythonProcess.SendResumeThread(thread.Id); + this.pythonProcess!.SendResumeThread(thread.Id); }); } else { - this.pythonProcess.SendResumeThread(thread.Id); + this.pythonProcess!.SendResumeThread(thread.Id); } } } @@ -201,6 +208,7 @@ export class PythonDebugger extends DebugSession { private canStartDebugger(): Promise { return Promise.resolve(true); } + @capturePerformanceTelemetry('launch') protected launchRequest(response: DebugProtocol.LaunchResponse, args: LaunchRequestArguments): void { // Some versions may still exist with incorrect launch.json values const setting = '${config.python.pythonPath}'; @@ -258,20 +266,19 @@ export class PythonDebugger extends DebugSession { this.launchArgs = args; this.debugClient = CreateLaunchDebugClient(args, this, this._supportsRunInTerminalRequest); - //this.debugClient.on('exit', () => this.sendEvent(new TerminatedEvent())); this.configurationDone = new Promise(resolve => { this.configurationDonePromiseResolve = resolve; }); this.entryResponse = response; - let that = this; + const that = this; this.startDebugServer().then(dbgServer => { - return that.debugClient.LaunchApplicationToDebug(dbgServer, that.unhandledProcessError.bind(that)); + return that.debugClient!.LaunchApplicationToDebug(dbgServer, that.unhandledProcessError.bind(that)); }).catch(error => { - this.sendEvent(new OutputEvent(error + "\n", "stderr")); + this.sendEvent(new OutputEvent(`${error}${'\n'}`, "stderr")); response.success = false; - let errorMsg = typeof error === "string" ? error : ((error.message && error.message.length > 0) ? error.message : error + ''); + let errorMsg = typeof error === "string" ? error : ((error.message && error.message.length > 0) ? error.message : error); if (isNotInstalledError(error)) { errorMsg = `Failed to launch the Python Process, please validate the path '${this.launchArgs.pythonPath}'`; } @@ -285,7 +292,7 @@ export class PythonDebugger extends DebugSession { errorMsg = `Failed to launch the Python Process, please validate the path '${this.launchArgs.pythonPath}'`; } if (errorMsg.length > 0) { - this.sendEvent(new OutputEvent(errorMsg + "\n", "stderr")); + this.sendEvent(new OutputEvent(`${errorMsg}${'\n'}`, "stderr")); } this.terminateEventSent = true; this.sendEvent(new TerminatedEvent()); @@ -296,47 +303,41 @@ export class PythonDebugger extends DebugSession { this.attachArgs = args; this.debugClient = CreateAttachDebugClient(args, this); this.entryResponse = response; - let that = this; + const that = this; this.canStartDebugger().then(() => { return this.startDebugServer(); }).then(dbgServer => { - return that.debugClient.LaunchApplicationToDebug(dbgServer, () => { }); + return that.debugClient!.LaunchApplicationToDebug(dbgServer, () => { }); }).catch(error => { - this.sendEvent(new OutputEvent(error + "\n", "stderr")); - this.sendErrorResponse(that.entryResponse, 2000, error); + this.sendEvent(new OutputEvent(`${error}${'\n'}`, "stderr")); + this.sendErrorResponse(that.entryResponse!, 2000, error); }); } protected configurationDoneRequest(response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void { // Tell debugger we have loaded the breakpoints if (this.configurationDonePromiseResolve) { - this.configurationDonePromiseResolve(); - this.configurationDonePromiseResolve = null; + this.configurationDonePromiseResolve!(); + this.configurationDonePromiseResolve = undefined; } this.sendResponse(response); } private onBreakpointHit(pyThread: IPythonThread, breakpointId: number) { // Break only if the breakpoint exists and it is enabled - if (this.registeredBreakpoints.has(breakpointId) && this.registeredBreakpoints.get(breakpointId).Enabled === true) { + if (this.registeredBreakpoints.has(breakpointId) && this.registeredBreakpoints.get(breakpointId)!.Enabled === true) { this.sendEvent(new StoppedEvent("breakpoint", pyThread.Id)); } else { - this.pythonProcess.SendResumeThread(pyThread.Id); + this.pythonProcess!.SendResumeThread(pyThread.Id); } } private buildBreakpointDetails(filePath: string, line: number, condition: string): IPythonBreakpoint { let isDjangoFile = false; - if (this.launchArgs != null && + if (this.launchArgs && Array.isArray(this.launchArgs.debugOptions) && this.launchArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0) { isDjangoFile = filePath.toUpperCase().endsWith(".HTML"); } - // Todo: Remote DJango debugging - // if (this.attachArgs != null && - // Array.isArray(this.attachArgs.debugOptions) && - // this.attachArgs.debugOptions.indexOf(DebugOptions.DjangoDebugging) >= 0) { - // isDjangoFile = filePath.toUpperCase().endsWith(".HTML"); - // } condition = typeof condition === "string" ? condition : ""; @@ -344,7 +345,7 @@ export class PythonDebugger extends DebugSession { Condition: condition, ConditionKind: condition.length === 0 ? PythonBreakpointConditionKind.Always : PythonBreakpointConditionKind.WhenTrue, Filename: filePath, - Id: this.breakPointCounter++, + Id: this.breakPointCounter += 1, LineNo: line, PassCount: 0, PassCountKind: PythonBreakpointPassCountKind.Always, @@ -360,24 +361,23 @@ export class PythonDebugger extends DebugSession { }; return this.sendResponse(response); } - if (!this.registeredBreakpointsByFileName.has(args.source.path)) { - this.registeredBreakpointsByFileName.set(args.source.path, []); + if (!this.registeredBreakpointsByFileName.has(args.source.path!)) { + this.registeredBreakpointsByFileName.set(args.source.path!, []); } - let breakpoints: { verified: boolean, line: number }[] = []; - let linesToAdd = args.breakpoints.map(b => b.line); - let registeredBks = this.registeredBreakpointsByFileName.get(args.source.path); - let linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); - // let linesToUpdate = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) >= 0); + const breakpoints: { verified: boolean, line: number }[] = []; + const linesToAdd = args.breakpoints!.map(b => b.line); + const registeredBks = this.registeredBreakpointsByFileName.get(args.source.path!)!; + const linesToRemove = registeredBks.map(b => b.LineNo).filter(oldLine => linesToAdd.indexOf(oldLine) === -1); - // Always add new breakpoints, don't re-enable previous breakpoints + // Always add new breakpoints, don't re-enable previous breakpoints, // Cuz sometimes some breakpoints get added too early (e.g. in django) and don't get registeredBks - // and the response comes back indicating it wasn't set properly + // and the response comes back indicating it wasn't set properly. // However, at a later point in time, the program breaks at that point!!! - let linesToAddPromises = args.breakpoints.map(bk => { + const linesToAddPromises = args.breakpoints!.map(bk => { return new Promise(resolve => { let breakpoint: IPythonBreakpoint; - let existingBreakpointsForThisLine = registeredBks.filter(registeredBk => registeredBk.LineNo === bk.line); + const existingBreakpointsForThisLine = registeredBks.filter(registeredBk => registeredBk.LineNo === bk.line); if (existingBreakpointsForThisLine.length > 0) { // We have an existing breakpoint for this line // just enable that @@ -385,10 +385,10 @@ export class PythonDebugger extends DebugSession { breakpoint.Enabled = true; } else { - breakpoint = this.buildBreakpointDetails(this.convertClientPathToDebugger(args.source.path), bk.line, bk.condition); + breakpoint = this.buildBreakpointDetails(this.convertClientPathToDebugger(args.source.path!), bk.line, bk.condition!); } - this.pythonProcess.BindBreakpoint(breakpoint).then(() => { + this.pythonProcess!.BindBreakpoint(breakpoint).then(() => { this.registeredBreakpoints.set(breakpoint.Id, breakpoint); breakpoints.push({ verified: true, line: bk.line }); registeredBks.push(breakpoint); @@ -402,18 +402,18 @@ export class PythonDebugger extends DebugSession { }); }); - let linesToRemovePromises = linesToRemove.map(line => { + const linesToRemovePromises = linesToRemove.map(line => { return new Promise(resolve => { - let registeredBks = this.registeredBreakpointsByFileName.get(args.source.path); - let bk = registeredBks.filter(b => b.LineNo === line)[0]; + const bookmarks = this.registeredBreakpointsByFileName.get(args.source.path!)!; + const bk = bookmarks.filter(b => b.LineNo === line)[0]; // Ok, we won't get a response back, so update the breakpoints list indicating this has been disabled bk.Enabled = false; - this.pythonProcess.DisableBreakPoint(bk); + this.pythonProcess!.DisableBreakPoint(bk); resolve(); }); }); - let promises = linesToAddPromises.concat(linesToRemovePromises); + const promises = linesToAddPromises.concat(linesToRemovePromises); Promise.all(promises).then(() => { response.body = { breakpoints: breakpoints @@ -423,17 +423,17 @@ export class PythonDebugger extends DebugSession { // Tell debugger we have loaded the breakpoints if (this.configurationDonePromiseResolve) { - this.configurationDonePromiseResolve(); - this.configurationDonePromiseResolve = null; + this.configurationDonePromiseResolve!(); + this.configurationDonePromiseResolve = undefined; } }).catch(error => this.sendErrorResponse(response, 2000, error)); }); } protected threadsRequest(response: DebugProtocol.ThreadsResponse): void { - let threads = []; + const threads: Thread[] = []; if (this.pythonProcess) { - this.pythonProcess.Threads.forEach(t => { + this.pythonProcess!.Threads.forEach(t => { threads.push(new Thread(t.Id, t.Name)); }); } @@ -443,22 +443,18 @@ export class PythonDebugger extends DebugSession { }; this.sendResponse(response); } - /** converts the remote path to local path */ protected convertDebuggerPathToClient(remotePath: string): string { if (this.attachArgs && this.attachArgs.localRoot && this.attachArgs.remoteRoot) { let path2 = path.win32; if (this.attachArgs.remoteRoot.indexOf('/') !== -1) { path2 = path.posix; } - let pathRelativeToSourceRoot = path2.relative(this.attachArgs.remoteRoot, remotePath); - // resolve from the local source root - let clientPath = path.resolve(this.attachArgs.localRoot, pathRelativeToSourceRoot); - return clientPath; + const pathRelativeToSourceRoot = path2.relative(this.attachArgs.remoteRoot, remotePath); + return path.resolve(this.attachArgs.localRoot, pathRelativeToSourceRoot); } else { return remotePath; } } - /** converts the local path to remote path */ protected convertClientPathToDebugger(clientPath: string): string { if (this.attachArgs && this.attachArgs.localRoot && this.attachArgs.remoteRoot) { // get the part of the path that is relative to the client root @@ -475,20 +471,20 @@ export class PythonDebugger extends DebugSession { } protected stackTraceRequest(response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void { this.debuggerLoaded.then(() => { - if (this.terminateEventSent || !this.pythonProcess || !this.pythonProcess.Threads.has(args.threadId)) { + if (this.terminateEventSent || !this.pythonProcess || !this.pythonProcess!.Threads.has(args.threadId)) { response.body = { stackFrames: [] }; return this.sendResponse(response); } - let pyThread = this.pythonProcess.Threads.get(args.threadId); + const pyThread = this.pythonProcess!.Threads.get(args.threadId)!; let maxFrames = typeof args.levels === "number" && args.levels > 0 ? args.levels : pyThread.Frames.length - 1; maxFrames = maxFrames < pyThread.Frames.length ? maxFrames : pyThread.Frames.length; - let frames = pyThread.Frames.map(frame => { + const frames = pyThread.Frames.map(frame => { return validatePath(this.convertDebuggerPathToClient(frame.FileName)).then(fileName => { - let frameId = this._pythonStackFrames.create(frame); + const frameId = this._pythonStackFrames.create(frame); if (fileName.length === 0) { return new StackFrame(frameId, frame.FunctionName); } @@ -509,35 +505,39 @@ export class PythonDebugger extends DebugSession { }); }); } + @capturePerformanceTelemetry('stepIn') protected stepInRequest(response: DebugProtocol.StepInResponse): void { this.sendResponse(response); - this.pythonProcess.SendStepInto(this.pythonProcess.LastExecutedThread.Id); + this.pythonProcess!.SendStepInto(this.pythonProcess!.LastExecutedThread.Id); } + @capturePerformanceTelemetry('stepOut') protected stepOutRequest(response: DebugProtocol.StepInResponse): void { this.sendResponse(response); - this.pythonProcess.SendStepOut(this.pythonProcess.LastExecutedThread.Id); + this.pythonProcess!.SendStepOut(this.pythonProcess!.LastExecutedThread.Id); } + @capturePerformanceTelemetry('continue') protected continueRequest(response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void { - this.pythonProcess.SendContinue().then(() => { + this.pythonProcess!.SendContinue().then(() => { this.sendResponse(response); }).catch(error => this.sendErrorResponse(response, 2000, error)); } + @capturePerformanceTelemetry('next') protected nextRequest(response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void { this.sendResponse(response); - this.pythonProcess.SendStepOver(this.pythonProcess.LastExecutedThread.Id); + this.pythonProcess!.SendStepOver(this.pythonProcess!.LastExecutedThread.Id); } protected evaluateRequest(response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): void { this.debuggerLoaded.then(() => { - let frame = this._pythonStackFrames.get(args.frameId); + const frame = this._pythonStackFrames.get(args.frameId!)!; if (this.terminateEventSent || !frame || !this.pythonProcess) { response.body = { - result: null, + result: '', variablesReference: 0 }; return this.sendResponse(response); } - this.pythonProcess.ExecuteText(args.expression, PythonEvaluationResultReprKind.Normal, frame).then(result => { + this.pythonProcess!.ExecuteText(args.expression, PythonEvaluationResultReprKind.Normal, frame).then(result => { let variablesReference = 0; // If this value can be expanded, then create a vars ref for user to expand it if (result.IsExpandable) { @@ -558,7 +558,7 @@ export class PythonDebugger extends DebugSession { } protected scopesRequest(response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void { this.debuggerLoaded.then(() => { - let frame = this._pythonStackFrames.get(args.frameId); + const frame = this._pythonStackFrames.get(args.frameId)!; if (this.terminateEventSent || !frame || !this.pythonProcess) { response.body = { scopes: [] @@ -566,34 +566,34 @@ export class PythonDebugger extends DebugSession { return this.sendResponse(response); } - let scopes = []; - if (typeof this.lastException === 'object' && this.lastException !== null && this.lastException.Description.length > 0) { - let values: IDebugVariable = { + const scopes: Scope[] = []; + if (this.lastException && this.lastException!.Description.length > 0) { + const values: IDebugVariable = { variables: [{ Frame: frame, Expression: 'Type', Flags: PythonEvaluationResultFlags.Raw, - StringRepr: this.lastException.TypeName, + StringRepr: this.lastException!.TypeName, TypeName: 'string', IsExpandable: false, HexRepr: '', - ChildName: '', ExceptionText: '', Length: 0, Process: null + ChildName: '', ExceptionText: '', Length: 0, Process: undefined }, { Frame: frame, Expression: 'Description', Flags: PythonEvaluationResultFlags.Raw, - StringRepr: this.lastException.Description, + StringRepr: this.lastException!.Description, TypeName: 'string', IsExpandable: false, HexRepr: '', - ChildName: '', ExceptionText: '', Length: 0, Process: null + ChildName: '', ExceptionText: '', Length: 0, Process: undefined }], evaluateChildren: false }; scopes.push(new Scope("Exception", this._variableHandles.create(values), false)); - this.lastException = null; + this.lastException = undefined; } if (Array.isArray(frame.Locals) && frame.Locals.length > 0) { - let values: IDebugVariable = { variables: frame.Locals }; + const values: IDebugVariable = { variables: frame.Locals }; scopes.push(new Scope("Local", this._variableHandles.create(values), false)); } if (Array.isArray(frame.Parameters) && frame.Parameters.length > 0) { - let values: IDebugVariable = { variables: frame.Parameters }; + const values: IDebugVariable = { variables: frame.Parameters }; scopes.push(new Scope("Arguments", this._variableHandles.create(values), false)); } response.body = { scopes }; @@ -601,10 +601,10 @@ export class PythonDebugger extends DebugSession { }); } protected variablesRequest(response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments): void { - let varRef = this._variableHandles.get(args.variablesReference); + const varRef = this._variableHandles.get(args.variablesReference)!; if (varRef.evaluateChildren !== true) { - let variables = []; + const variables: Variable[] = []; varRef.variables.forEach(variable => { let variablesReference = 0; // If this value can be expanded, then create a vars ref for user to expand it @@ -629,41 +629,42 @@ export class PythonDebugger extends DebugSession { return this.sendResponse(response); } - - // Ok, we need to evaluate the children of the current variable - let variables = []; - let promises = varRef.variables.map(variable => { - return variable.Process.EnumChildren(variable.Expression, variable.Frame, CHILD_ENUMEARATION_TIMEOUT).then(children => { - children.forEach(child => { - let variablesReference = 0; - // If this value can be expanded, then create a vars ref for user to expand it - if (child.IsExpandable) { - const childVariable: IDebugVariable = { - variables: [child], - evaluateChildren: true - }; - variablesReference = this._variableHandles.create(childVariable); - } - - variables.push({ - name: child.ChildName, - value: child.StringRepr, - variablesReference: variablesReference + else { + // Ok, we need to evaluate the children of the current variable. + const variables: Variable[] = []; + const promises = varRef.variables.map(variable => { + return variable.Process!.EnumChildren(variable.Expression, variable.Frame, CHILD_ENUMEARATION_TIMEOUT).then(children => { + children.forEach(child => { + let variablesReference = 0; + // If this value can be expanded, then create a vars ref for user to expand it + if (child.IsExpandable) { + const childVariable: IDebugVariable = { + variables: [child], + evaluateChildren: true + }; + variablesReference = this._variableHandles.create(childVariable); + } + + variables.push({ + name: child.ChildName, + value: child.StringRepr, + variablesReference: variablesReference + }); }); }); }); - }); - Promise.all(promises).then(() => { - response.body = { - variables: variables - }; + Promise.all(promises).then(() => { + response.body = { + variables: variables + }; - return this.sendResponse(response); - }).catch(error => this.sendErrorResponse(response, 2001, error)); + return this.sendResponse(response); + }).catch(error => this.sendErrorResponse(response, 2001, error)); + } } protected pauseRequest(response: DebugProtocol.PauseResponse): void { - this.pythonProcess.Break(); + this.pythonProcess!.Break(); this.sendResponse(response); } protected setExceptionBreakPointsRequest(response: DebugProtocol.SetExceptionBreakpointsResponse, args: DebugProtocol.SetExceptionBreakpointsArguments): void { @@ -678,10 +679,8 @@ export class PythonDebugger extends DebugSession { if (args.filters.indexOf("all") >= 0) { mode = enum_EXCEPTION_STATE.BREAK_MODE_ALWAYS; } - let exToIgnore = new Map(); - let exceptionHandling = this.launchArgs ? this.launchArgs.exceptionHandling : null; - // Todo: exception handling for remote debugging - // let exceptionHandling = this.launchArgs ? this.launchArgs.exceptionHandling : this.attachArgs.exceptionHandling; + const exToIgnore = new Map(); + const exceptionHandling = this.launchArgs ? this.launchArgs.exceptionHandling : null; if (exceptionHandling) { if (Array.isArray(exceptionHandling.ignore)) { exceptionHandling.ignore.forEach(exType => { @@ -709,7 +708,7 @@ export class PythonDebugger extends DebugSession { exToIgnore.set('GeneratorExit', enum_EXCEPTION_STATE.BREAK_MODE_NEVER); } if (this.pythonProcess) { - this.pythonProcess.SendExceptionInfo(mode, exToIgnore); + this.pythonProcess!.SendExceptionInfo(mode, exToIgnore); } this.sendResponse(response); }); @@ -719,20 +718,19 @@ export class PythonDebugger extends DebugSession { this.sendResponse(response); } protected setVariableRequest(response: DebugProtocol.SetVariableResponse, args: DebugProtocol.SetVariableArguments) { - let variable = this._variableHandles.get(args.variablesReference).variables.find(v => v.ChildName === args.name); + const variable = this._variableHandles.get(args.variablesReference)!.variables.find(v => v.ChildName === args.name); if (!variable) { return this.sendErrorResponse(response, 2000, 'Variable reference not found'); } - this.pythonProcess.ExecuteText(`${args.name} = ${args.value}`, PythonEvaluationResultReprKind.Normal, variable.Frame).then(result => { - return this.pythonProcess.ExecuteText(args.name, PythonEvaluationResultReprKind.Normal, variable.Frame).then(result => { - let variablesReference = 0; + this.pythonProcess!.ExecuteText(`${args.name} = ${args.value}`, PythonEvaluationResultReprKind.Normal, variable.Frame).then(() => { + return this.pythonProcess!.ExecuteText(args.name, PythonEvaluationResultReprKind.Normal, variable.Frame).then(result => { // If this value can be expanded, then create a vars ref for user to expand it if (result.IsExpandable) { const parentVariable: IDebugVariable = { variables: [result], evaluateChildren: true }; - variablesReference = this._variableHandles.create(parentVariable); + this._variableHandles.create(parentVariable); } response.body = { value: result.StringRepr diff --git a/src/client/debugger/ProxyCommands.ts b/src/client/debugger/ProxyCommands.ts index dd674de30534..1d3d3d4ac7dd 100644 --- a/src/client/debugger/ProxyCommands.ts +++ b/src/client/debugger/ProxyCommands.ts @@ -1,3 +1,4 @@ +// tslint:disable:quotemark no-stateless-class "use strict"; export class Commands { diff --git a/src/client/debugger/PythonProcess.ts b/src/client/debugger/PythonProcess.ts index 151581e6dbd3..ecb79bef0c3a 100644 --- a/src/client/debugger/PythonProcess.ts +++ b/src/client/debugger/PythonProcess.ts @@ -1,10 +1,11 @@ +// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self no-require-imports prefer-const "use strict"; import * as net from "net"; import { ChildProcess } from 'child_process'; import { EventEmitter } from "events"; -import { FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame, IStepCommand } from "./Common/Contracts"; -import { IPythonBreakpoint, PythonBreakpointConditionKind, PythonBreakpointPassCountKind, IBreakpointCommand, IChildEnumCommand } from "./Common/Contracts"; +import { IPythonProcess, IPythonThread, IPythonEvaluationResult, IPythonStackFrame } from "./Common/Contracts"; +import { IPythonBreakpoint, IBreakpointCommand, IChildEnumCommand } from "./Common/Contracts"; import { PythonEvaluationResultReprKind, IExecutionCommand, enum_EXCEPTION_STATE } from "./Common/Contracts"; import { Commands } from "./ProxyCommands"; import { IdDispenser } from "../common/idDispenser"; @@ -45,7 +46,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { public PendingExecuteCommands: Map; private executeCommandsQueue: IExecutionCommand[]; private callbackHandler: PythonProcessCallbackHandler; - private stream: SocketStream = null; + private stream: SocketStream; private programDirectory: string; public get ProgramDirectory(): string { return this.programDirectory; @@ -66,8 +67,8 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { if (!this.isRemoteProcess && this.pid && typeof this.pid === "number") { try { let kill = require("tree-kill"); - kill(this.pid); - this.pid = null; + kill(this.pid!); + this.pid = undefined; } catch (ex) { } } @@ -84,11 +85,11 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { private guidRead: boolean; private statusRead: boolean; private pidRead: boolean; - private pid: number; + private pid?: number; private isRemoteProcess: Boolean; public Connect(buffer: Buffer, socket: net.Socket, isRemoteProcess: boolean = false): boolean { this.isRemoteProcess = isRemoteProcess; - if (this.stream === null) { + if (!this.stream) { this.stream = new SocketStream(socket, buffer); } else { @@ -97,7 +98,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { if (!isRemoteProcess) { if (!this.guidRead) { this.stream.BeginTransaction(); - let guid = this.stream.ReadString(); + this.stream.ReadString(); if (this.stream.HasInsufficientDataForReading) { this.stream.RollBackTransaction(); return false; @@ -108,7 +109,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { if (!this.statusRead) { this.stream.BeginTransaction(); - let result = this.stream.ReadInt32(); + this.stream.ReadInt32(); if (this.stream.HasInsufficientDataForReading) { this.stream.RollBackTransaction(); return false; @@ -160,7 +161,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { if (!this.isRemoteProcess) { if (!this.guidRead) { this.stream.RollBackTransaction(); - let guid = this.stream.ReadString(); + this.stream.ReadString(); if (this.stream.HasInsufficientDataForReading) { return; } @@ -169,7 +170,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { } if (!this.statusRead) { this.stream.BeginTransaction(); - let result = this.stream.ReadInt32(); + this.stream.ReadInt32(); if (this.stream.HasInsufficientDataForReading) { this.stream.RollBackTransaction(); return; @@ -236,7 +237,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { } private onBreakpointSet(breakpointId: number, success: boolean) { // Find the last breakpoint command associated with this breakpoint - let index = this.breakpointCommands.findIndex(cmd => cmd.Id === breakpointId); + let index = this.breakpointCommands.findIndex(item => item.Id === breakpointId); if (index === -1) { // Hmm this is not possible, log this exception and carry on // this.emit("error", "command.breakpoint.hit", `Uknown Breakpoit Id ${breakpointId}`); @@ -348,7 +349,7 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { return; } - const cmd = this.executeCommandsQueue.shift(); + const cmd = this.executeCommandsQueue.shift()!; this.PendingExecuteCommands.set(cmd.Id, cmd); this.stream.Write(Commands.ExecuteTextCommandBytes); this.stream.WriteString(cmd.Text); @@ -362,9 +363,6 @@ export class PythonProcess extends EventEmitter implements IPythonProcess { public EnumChildren(text: string, stackFrame: IPythonStackFrame, timeout: number): Promise { return new Promise((resolve, reject) => { let executeId = this._idDispenser.Allocate(); - if (typeof (executeId) !== "number") { - let y = ""; - } let cmd: IChildEnumCommand = { Id: executeId, Frame: stackFrame, diff --git a/src/client/debugger/PythonProcessCallbackHandler.ts b/src/client/debugger/PythonProcessCallbackHandler.ts index 50e7bdf7b517..52935bfce481 100644 --- a/src/client/debugger/PythonProcessCallbackHandler.ts +++ b/src/client/debugger/PythonProcessCallbackHandler.ts @@ -1,6 +1,7 @@ +// tslint:disable:quotemark ordered-imports promise-must-complete member-ordering no-any prefer-template cyclomatic-complexity no-empty no-multiline-string one-line no-invalid-template-strings no-suspicious-comment no-var-self prefer-const no-single-line-block-comment no-unnecessary-local-variable "use strict"; -import { FrameKind, IPythonProcess, IPythonThread, IPythonModule, IPythonEvaluationResult, IPythonStackFrame } from "./Common/Contracts"; +import { FrameKind, IPythonProcess, IPythonThread, IPythonEvaluationResult, IPythonStackFrame } from "./Common/Contracts"; import { IDjangoStackFrame, PythonEvaluationResultFlags, PythonLanguageVersion, IChildEnumCommand, IPythonException, IExecutionCommand } from "./Common/Contracts"; import * as utils from "./Common/Utils"; import { EventEmitter } from "events"; @@ -8,13 +9,12 @@ import { Commands } from "./ProxyCommands"; import { SocketStream } from "../common/net/socket/SocketStream"; import { ExtractTryStatements } from "./Common/TryParser"; import * as path from "path"; -import {IdDispenser} from '../common/idDispenser'; +import { IdDispenser } from '../common/idDispenser'; export class PythonProcessCallbackHandler extends EventEmitter { private process: IPythonProcess; private idDispenser: IdDispenser; private stream: SocketStream; - private _stoppedForException: boolean; constructor(process: IPythonProcess, stream: SocketStream, idDispenser: IdDispenser) { super(); this.process = process; @@ -88,7 +88,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { if (this.stream.HasInsufficientDataForReading) { return; } - if (filename != null) { + if (filename) { this.emit("moduleLoaded", utils.CreatePythonModule(moduleId, filename)); } } @@ -99,9 +99,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } this.emit("output", pyThread, output); } @@ -125,9 +125,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let thread: IPythonThread; + let thread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - thread = this.process.Threads.get(threadId); + thread = this.process.Threads.get(threadId)!; this.emit("threadExited", thread); // this.process.Threads.delete(threadId); } @@ -138,9 +138,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { if (this.stream.HasInsufficientDataForReading) { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } this.emit("processLoaded", pyThread); } @@ -150,9 +150,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { if (this.stream.HasInsufficientDataForReading) { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } this.emit("stepCompleted", pyThread); } @@ -161,9 +161,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { if (this.stream.HasInsufficientDataForReading) { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } this.emit("asyncBreakCompleted", pyThread); } @@ -188,9 +188,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } this.emit("breakpointHit", pyThread, breakId); } @@ -226,7 +226,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { return ExtractTryStatements(fileName).then(statements => { let exceptionRanges: { startLine: number, endLine: number, expressions: string[] }[] = []; statements.forEach(statement => { - let expressions = []; + let expressions: string[] = []; if (statement.Exceptions.length === 0 || statement.Exceptions.indexOf("*") >= 0) { expressions = ["*"]; } @@ -258,40 +258,19 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - if (typeName != null && desc != null) { + if (typeName && desc) { let ex: IPythonException = { TypeName: typeName, Description: desc }; - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { pyThread = this.process.Threads.get(threadId); } - this.emit("exceptionRaised", pyThread, ex, breakType === 1 /* BREAK_TYPE_UNHANLDED */); + this.emit("exceptionRaised", pyThread!, ex, breakType === 1 /* BREAK_TYPE_UNHANLDED */); } - this._stoppedForException = true; } private HandleRichException() { - // let typeName = this.stream.ReadString(); - // let threadId = this.stream.ReadInt64(); - // let breakType = this.stream.ReadInt32(); - // let desc = this.stream.ReadString(); - // if (this.stream.HasInsufficientDataForReading) { - // return; - // } - - // if (typeName != null && desc != null) { - // let ex: IPythonException = { - // TypeName: typeName, - // Description: desc - // }; - // let pyThread: IPythonThread; - // if (this.process.Threads.has(threadId)) { - // pyThread = this.process.Threads.get(threadId); - // } - // this.emit("exceptionRaised", pyThread, ex, breakType === 1 /* BREAK_TYPE_UNHANLDED */); - // } - // this._stoppedForException = true; } private HandleExecutionException() { let execId = this.stream.ReadInt32(); @@ -300,13 +279,13 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let cmd: IExecutionCommand = null; + let cmd: IExecutionCommand | undefined; if (this.process.PendingExecuteCommands.has(execId)) { - cmd = this.process.PendingExecuteCommands.get(execId); + cmd = this.process.PendingExecuteCommands.get(execId)!; if (this.process.PendingExecuteCommands.has(execId)) { this.process.PendingExecuteCommands.delete(execId); } - cmd.PromiseReject(exceptionText); + cmd!.PromiseReject(exceptionText); } this.process.ProcessPendingExecuteCommands(); this.idDispenser.Free(execId); @@ -317,29 +296,29 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let cmd: IExecutionCommand = null; + let cmd: IExecutionCommand | undefined; if (this.process.PendingExecuteCommands.has(execId)) { - cmd = this.process.PendingExecuteCommands.get(execId); + cmd = this.process.PendingExecuteCommands.get(execId)!; } - if (cmd === null) { + if (!cmd) { // Passing null for parameters other than stream is okay as long // as we drop the result. - this.ReadPythonObject(null, null, null); + this.ReadPythonObject(null as any, null as any, null as any); if (this.stream.HasInsufficientDataForReading) { return; } } else { - let evalResult = this.ReadPythonObject(cmd.Text, null, cmd.Frame); + let evalResult = this.ReadPythonObject(cmd!.Text, null as any, cmd!.Frame); if (this.stream.HasInsufficientDataForReading) { return; } - cmd.PromiseResolve(evalResult); + cmd!.PromiseResolve(evalResult!); } - if (cmd != null) { + if (cmd) { if (this.process.PendingExecuteCommands.has(execId)) { this.process.PendingExecuteCommands.delete(execId); } @@ -354,9 +333,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let cmd: IChildEnumCommand = null; + let cmd: IChildEnumCommand | undefined; if (this.process.PendingChildEnumCommands.has(execId)) { - cmd = this.process.PendingChildEnumCommands.get(execId); + cmd = this.process.PendingChildEnumCommands.get(execId)!; } let childrenCount = this.stream.ReadInt32(); @@ -365,22 +344,22 @@ export class PythonProcessCallbackHandler extends EventEmitter { } const children: IPythonEvaluationResult[] = []; - for (let childCount = 0; childCount < childrenCount; childCount++) { + for (let childCount = 0; childCount < childrenCount; childCount += 1) { const childName = this.stream.ReadString(); const childExpr = this.stream.ReadString(); if (this.stream.HasInsufficientDataForReading) { return; } - let obj = this.ReadPythonObject(childExpr, childName, cmd === null ? null : cmd.Frame); + let obj = this.ReadPythonObject(childExpr, childName, cmd ? cmd!.Frame : null as any); if (this.stream.HasInsufficientDataForReading) { return; } - children.push(obj); + children.push(obj!); } - if (cmd != null) { - cmd.PromiseResolve(children); + if (cmd) { + cmd!.PromiseResolve(children); if (this.process.PendingChildEnumCommands.has(execId)) { this.process.PendingChildEnumCommands.delete(execId); } @@ -394,9 +373,9 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let pyThread: IPythonThread; + let pyThread: IPythonThread | undefined; if (this.process.Threads.has(threadId)) { - pyThread = this.process.Threads.get(threadId); + pyThread = this.process.Threads.get(threadId)!; } let threadName = this.stream.ReadString(); @@ -405,7 +384,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - for (let i = 0; i < frameCount; i++) { + for (let i = 0; i < frameCount; i += 1) { let startLine = this.stream.ReadInt32(); let endLine = this.stream.ReadInt32(); let lineNo = this.stream.ReadInt32(); @@ -417,8 +396,8 @@ export class PythonProcessCallbackHandler extends EventEmitter { return; } - let frame: IPythonStackFrame = null; - if (pyThread != null) { + let frame: IPythonStackFrame | undefined; + if (pyThread) { switch (frameKind) { case FrameKind.Django: { let sourceFile = this.stream.ReadString(); @@ -432,7 +411,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { FrameId: i, FunctionName: frameName, Kind: frameKind, LineNo: lineNo, Locals: [], Parameters: [], - Thread: pyThread, SourceFile: sourceFile, + Thread: pyThread!, SourceFile: sourceFile, SourceLine: sourceLine, StartLine: startLine }; @@ -445,7 +424,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { FrameId: i, FunctionName: frameName, Kind: frameKind, LineNo: lineNo, Locals: [], Parameters: [], - Thread: pyThread, StartLine: startLine + Thread: pyThread!, StartLine: startLine }; break; } @@ -459,37 +438,37 @@ export class PythonProcessCallbackHandler extends EventEmitter { } let variables: IPythonEvaluationResult[] = []; - for (let j = 0; j < varCount; j++) { + for (let j = 0; j < varCount; j += 1) { let name = this.stream.ReadString(); if (this.stream.HasInsufficientDataForReading) { return; } - if (frame != null) { - let variableObj = this.ReadPythonObject(name, name, frame); + if (frame) { + let variableObj = this.ReadPythonObject(name, name, frame!); if (this.stream.HasInsufficientDataForReading) { return; } - variables.push(variableObj); + variables.push(variableObj!); } } - if (frame != null) { - frame.Parameters = variables.splice(0, argCount); - frame.Locals = variables; - frames.push(frame); + if (frame) { + frame!.Parameters = variables.splice(0, argCount); + frame!.Locals = variables; + frames.push(frame!); } } - if (pyThread != null) { - pyThread.Frames = frames; + if (pyThread) { + pyThread!.Frames = frames; if (typeof threadName === "string" && threadName.length > 0) { - pyThread.Name = threadName; + pyThread!.Name = threadName; } } } - private ReadPythonObject(expr: string, childName: string, frame: IPythonStackFrame): IPythonEvaluationResult { + private ReadPythonObject(expr: string, childName: string, frame: IPythonStackFrame): IPythonEvaluationResult | undefined { let objRepr = this.stream.ReadString(); let hexRepr = this.stream.ReadString(); let typeName = this.stream.ReadString(); @@ -506,7 +485,7 @@ export class PythonProcessCallbackHandler extends EventEmitter { } if (typeName === "bool") { - hexRepr = null; + hexRepr = null as any; } let pythonEvaluationResult: IPythonEvaluationResult = { diff --git a/src/client/debugger/configProviders/simpleProvider.ts b/src/client/debugger/configProviders/simpleProvider.ts index bde83ba66b30..1be85b735f82 100644 --- a/src/client/debugger/configProviders/simpleProvider.ts +++ b/src/client/debugger/configProviders/simpleProvider.ts @@ -1,6 +1,6 @@ import * as path from 'path'; -import { PythonSettings } from '../../common/configSettings'; import { CancellationToken, DebugConfiguration, DebugConfigurationProvider, ProviderResult, Uri, window, workspace, WorkspaceFolder } from 'vscode'; +import { PythonSettings } from '../../common/configSettings'; type PythonDebugConfiguration = DebugConfiguration & { stopOnEntry?: boolean, @@ -37,7 +37,8 @@ export class SimpleConfigurationProvider implements DebugConfigurationProvider { return undefined; } - resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { + // tslint:disable-next-line:member-ordering + public resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): ProviderResult { const keys = Object.keys(debugConfiguration); const provideConfig = (debugConfiguration.noDebug === true && keys.length === 1) || keys.length === 0; if (!provideConfig) { diff --git a/src/client/debugger/index.ts b/src/client/debugger/index.ts index d2343dd26a9e..9065d5fdeb02 100644 --- a/src/client/debugger/index.ts +++ b/src/client/debugger/index.ts @@ -1 +1 @@ -export * from './configProviders/simpleProvider'; \ No newline at end of file +export * from './configProviders/simpleProvider'; diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 80ec145a1bfd..bf02b07c63c7 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -25,6 +25,7 @@ export const WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO'; export const EXECUTION_CODE = 'EXECUTION_CODE'; export const EXECUTION_DJANGO = 'EXECUTION_DJANGO'; export const DEBUGGER = 'DEBUGGER'; +export const DEBUGGER_PERFORMANCE = 'DEBUGGER.PERFORMANCE'; export const UNITTEST_STOP = 'UNITTEST.STOP'; export const UNITTEST_RUN = 'UNITTEST.RUN'; export const UNITTEST_DISCOVER = 'UNITTEST.DISCOVER'; diff --git a/src/client/telemetry/types.ts b/src/client/telemetry/types.ts index 9ddfa963ab2c..3ee528928624 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -31,6 +31,10 @@ export type DebuggerTelemetry = { pyspark?: boolean; hasEnvVars?: boolean; }; +export type DebuggerPerformanceTelemetry = { + duration: number; + action: 'stepIn' | 'stepOut' | 'continue' | 'next' | 'launch'; +}; export type TestRunTelemetry = { tool: 'nosetest' | 'pytest' | 'unittest' scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; diff --git a/src/test/common.ts b/src/test/common.ts index 98bbb44364b5..3c59c4235d8f 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -109,6 +109,10 @@ export async function deleteFile(file: string) { } } +export async function sleep(milliseconds: number) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + // tslint:disable-next-line:no-non-null-assertion const globalPythonPathSetting = workspace.getConfiguration('python').inspect('pythonPath')!.globalValue; export const clearPythonPathInWorkspaceFolder = async (resource: string | Uri) => retryAsync(setPythonPathInWorkspace)(resource, ConfigurationTarget.WorkspaceFolder); diff --git a/src/test/debugger/perfTelemetry.test.ts b/src/test/debugger/perfTelemetry.test.ts new file mode 100644 index 000000000000..265a9a8dc906 --- /dev/null +++ b/src/test/debugger/perfTelemetry.test.ts @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:max-func-body-length no-use-before-declare + +import { expect } from 'chai'; +import { DebugSession } from 'vscode-debugadapter'; +import { StoppedEvent } from 'vscode-debugadapter/lib/debugSession'; +import { DebugProtocol } from 'vscode-debugprotocol'; +import { TelemetryEvent } from '../../client/debugger/Common/Contracts'; +import { + capturePerformanceTelemetry, + PerformanceTelemetryCondition, + sendPerformanceTelemetry +} from '../../client/debugger/Common/telemetry'; +import { DebuggerPerformanceTelemetry } from '../../client/telemetry/types'; +import { sleep } from '../common'; +import { initialize } from '../initialize'; + +suite('Debugging - Performance Telemetry', () => { + suiteSetup(initialize); + setup(() => MockDebugSession.TelemetryEvents = []); + + function testTelemetryEvents(expectedActions: string[]) { + expect(MockDebugSession.TelemetryEvents).lengthOf(expectedActions.length, 'Incorrect number of events'); + const actions = MockDebugSession.TelemetryEvents.map(item => (item.body.data as DebuggerPerformanceTelemetry).action); + expect(actions).deep.equal(expectedActions, 'Incorrect actions'); + } + + test('Event = load', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + await sleep(501); + session.onPythonProcessLoaded(); + testTelemetryEvents(['launch']); + expect((MockDebugSession.TelemetryEvents[0].body.data as DebuggerPerformanceTelemetry).duration).greaterThan(500, 'incorrect duration'); + }); + + test('Event = stopped for stepin', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + session.onPythonProcessLoaded(); + session.stepInRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + + testTelemetryEvents(['launch', 'stepIn']); + }); + + test('Event = stopped for stepout', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + session.onPythonProcessLoaded(); + session.stepOutRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + + testTelemetryEvents(['launch', 'stepOut']); + }); + + test('Event = stopped for continue', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + session.onPythonProcessLoaded(); + session.continueRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + + testTelemetryEvents(['launch', 'continue']); + }); + + test('Event = stopped for next', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + session.onPythonProcessLoaded(); + session.nextRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + + testTelemetryEvents(['launch', 'next']); + }); + + test('Event = stopped for stepout, next, stepin', async () => { + const session = new MockDebugSession(); + session.launchRequest(); + session.onPythonProcessLoaded(); + session.stepOutRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + session.nextRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + session.stepInRequest(); + session.sendEvent(new StoppedEvent('some reason', 0)); + + testTelemetryEvents(['launch', 'stepOut', 'next', 'stepIn']); + }); +}); + +class MockDebugSession extends DebugSession { + public static TelemetryEvents: TelemetryEvent[] = []; + constructor() { + super(); + } + + @capturePerformanceTelemetry('launch') + // tslint:disable-next-line:no-empty + public launchRequest(): void { + } + // tslint:disable-next-line:no-unnecessary-override + @sendPerformanceTelemetry(PerformanceTelemetryCondition.stoppedEvent) + // tslint:disable-next-line:no-empty + public sendEvent(event: DebugProtocol.Event): void { + if (event instanceof TelemetryEvent) { + MockDebugSession.TelemetryEvents.push(event); + } + } + @sendPerformanceTelemetry(PerformanceTelemetryCondition.always) + // tslint:disable-next-line:no-empty + public onPythonProcessLoaded() { + } + @capturePerformanceTelemetry('stepIn') + // tslint:disable-next-line:no-empty + public stepInRequest(): void { + } + @capturePerformanceTelemetry('stepOut') + // tslint:disable-next-line:no-empty + public stepOutRequest(): void { + } + @capturePerformanceTelemetry('continue') + // tslint:disable-next-line:no-empty + public continueRequest(): void { + } + @capturePerformanceTelemetry('next') + // tslint:disable-next-line:no-empty + public nextRequest(): void { + } +} diff --git a/src/test/definitions/hover.test.ts b/src/test/definitions/hover.test.ts index adc7832b80fc..7e64db251464 100644 --- a/src/test/definitions/hover.test.ts +++ b/src/test/definitions/hover.test.ts @@ -1,11 +1,5 @@ -// Note: This example test is leveraging the Mocha test framework. -// Please refer to their documentation on https://mochajs.org/ for help. - -// The module 'assert' provides assertion methods from node import * as assert from 'assert'; import { EOL } from 'os'; -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it import * as path from 'path'; import * as vscode from 'vscode'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; @@ -28,20 +22,19 @@ suite('Hover Definition', () => { teardown(closeActiveWindows); test('Method', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileOne).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(30, 5); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '30,4', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '30,11', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); assert.equal(def[0].contents.length, 1, 'Invalid content items'); // tslint:disable-next-line:prefer-template const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; @@ -50,40 +43,38 @@ suite('Hover Definition', () => { }); test('Across files', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileThree).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(1, 12); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,9', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,12', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,9', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def fun()' + EOL + '```' + EOL + 'This is fun', 'Invalid conents'); }).then(done, done); }); test('With Unicode Characters', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileEncoding).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(25, 6); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '25,4', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '25,7', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,4', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def bar()' + EOL + '```' + EOL + '说明 - keep this line, it works' + EOL + 'delete following line, it works' + @@ -92,20 +83,19 @@ suite('Hover Definition', () => { }); test('Across files with Unicode Characters', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileEncodingUsed).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(1, 11); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '1,5', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '1,16', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,5', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def showMessage()' + EOL + @@ -116,52 +106,47 @@ suite('Hover Definition', () => { }); test('Nothing for keywords (class)', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileOne).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(5, 1); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 0, 'Definition length is incorrect'); + assert.equal(def!.length, 0, 'Definition length is incorrect'); }).then(done, done); }); test('Nothing for keywords (for)', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(3, 1); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); }).then(def => { - assert.equal(def.length, 0, 'Definition length is incorrect'); + assert.equal(def!.length, 0, 'Definition length is incorrect'); }).then(done, done); }); test('Highlighting Class', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(11, 15); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '11,12', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '11,18', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,12', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); // tslint:disable-next-line:prefer-template const documentation = '```python' + EOL + 'class Random(x=None)' + EOL + @@ -182,20 +167,19 @@ suite('Hover Definition', () => { }); test('Highlight Method', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(12, 10); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '12,5', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '12,12', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,5', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def randint(a, b)' + EOL + @@ -205,20 +189,19 @@ suite('Hover Definition', () => { }); test('Highlight Function', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(8, 14); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '8,11', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '8,15', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,11', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'def acos(x)' + EOL + @@ -228,20 +211,19 @@ suite('Hover Definition', () => { }); test('Highlight Multiline Method Signature', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(14, 14); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range.start.line},${def[0].range.start.character}`, '14,9', 'Start position is incorrect'); - assert.equal(`${def[0].range.end.line},${def[0].range.end.character}`, '14,15', 'End position is incorrect'); + assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,9', 'Start position is incorrect'); + assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); // tslint:disable-next-line:prefer-template assert.equal(normalizeMarkedString(def[0].contents[0]), '```python' + EOL + 'class Thread(group=None, target=None, name=None, args=(), kwargs=None, verbose=None)' + EOL + @@ -256,17 +238,16 @@ suite('Hover Definition', () => { }); test('Variable', done => { - let textEditor: vscode.TextEditor; let textDocument: vscode.TextDocument; vscode.workspace.openTextDocument(fileHover).then(document => { textDocument = document; return vscode.window.showTextDocument(textDocument); }).then(editor => { assert(vscode.window.activeTextEditor, 'No active editor'); - textEditor = editor; const position = new vscode.Position(6, 2); return vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { + }).then(result => { + const def = result!; assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(def[0].contents.length, 1, 'Only expected one result'); const contents = normalizeMarkedString(def[0].contents[0]); @@ -286,7 +267,7 @@ suite('Hover Definition', () => { const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); await vscode.window.showTextDocument(textDocument); const position = new vscode.Position(5, 41); - const def = await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position); + const def = (await vscode.commands.executeCommand('vscode.executeHoverProvider', textDocument.uri, position))!; assert.equal(def.length, 1, 'Definition length is incorrect'); assert.equal(def[0].contents.length, 1, 'Only expected one result'); const contents = normalizeMarkedString(def[0].contents[0]);