| // Copyright 2022 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as Bindings from '../../models/bindings/bindings.js'; |
| import * as TextUtils from '../../models/text_utils/text_utils.js'; |
| import * as Workspace from '../../models/workspace/workspace.js'; |
| import {createTarget, describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; |
| import {describeWithMockConnection} from '../../testing/MockConnection.js'; |
| import {MockProtocolBackend, parseScopeChain} from '../../testing/MockScopeChain.js'; |
| import * as CodeMirror from '../../third_party/codemirror.next/codemirror.next.js'; |
| import * as TextEditor from '../../ui/components/text_editor/text_editor.js'; |
| |
| import * as Sources from './sources.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| |
| describeWithMockConnection('Inline variable view scope helpers', () => { |
| const URL = urlString`file:///tmp/example.js`; |
| let target: SDK.Target.Target; |
| let backend: MockProtocolBackend; |
| |
| beforeEach(() => { |
| const workspace = Workspace.Workspace.WorkspaceImpl.instance(); |
| const targetManager = SDK.TargetManager.TargetManager.instance(); |
| const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); |
| const ignoreListManager = Workspace.IgnoreListManager.IgnoreListManager.instance({forceNew: true}); |
| Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ |
| forceNew: true, |
| resourceMapping, |
| targetManager, |
| ignoreListManager, |
| workspace, |
| }); |
| target = createTarget(); |
| backend = new MockProtocolBackend(); |
| }); |
| |
| async function toOffsetWithSourceMap( |
| sourceMap: SDK.SourceMap.SourceMap|undefined, location: SDK.DebuggerModel.Location|null) { |
| if (!location || !sourceMap) { |
| return null; |
| } |
| const entry = sourceMap.findEntry(location.lineNumber, location.columnNumber); |
| if (!entry?.sourceURL) { |
| return null; |
| } |
| const content = sourceMap.embeddedContentByURL(entry.sourceURL); |
| if (!content) { |
| return null; |
| } |
| const text = new TextUtils.Text.Text(content); |
| return text.offsetFromPosition(entry.sourceLineNumber, entry.sourceColumnNumber); |
| } |
| |
| async function toOffset(source: string|null, location: SDK.DebuggerModel.Location|null) { |
| if (!location || !source) { |
| return null; |
| } |
| const text = new TextUtils.Text.Text(source); |
| return text.offsetFromPosition(location.lineNumber, location.columnNumber); |
| } |
| |
| it('can resolve single scope mappings with source map', async () => { |
| const sourceMapUrl = 'file:///tmp/example.js.min.map'; |
| // This example was minified with terser v5.7.0 with following command. |
| // 'terser index.js -m --toplevel -o example.min.js --source-map "url=example.min.js.map,includeSources"' |
| const source = `function o(o,n){console.log(o,n)}o(1,2);\n//# sourceMappingURL=${sourceMapUrl}`; |
| const scopes = ' { }'; |
| |
| // The original scopes below have to match with how the source map translates the scope, so it |
| // does not align perfectly with the source language scopes. In principle, this test could only |
| // assert that the tests are approximately correct; currently, we assert an exact match. |
| const originalSource = 'function unminified(par1, par2) {\n console.log(par1, par2);\n}\nunminified(1, 2);\n'; |
| const originalScopes = ' { \n \n }'; |
| const expectedOffsets = parseScopeChain(originalScopes); |
| |
| const sourceMapContent = { |
| version: 3, |
| names: ['unminified', 'par1', 'par2', 'console', 'log'], |
| sources: ['index.js'], |
| sourcesContent: [originalSource], |
| mappings: 'AAAA,SAASA,EAAWC,EAAMC,GACxBC,QAAQC,IAAIH,EAAMC,EACpB,CACAF,EAAW,EAAG', |
| }; |
| const sourceMapJson = JSON.stringify(sourceMapContent); |
| |
| const scopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 42}, {name: 'n', value: 1}]); |
| const callFrame = await backend.createCallFrame( |
| target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapJson}, [scopeObject]); |
| |
| // Get source map for mapping locations to 'editor' offsets. |
| const sourceMap = await callFrame.debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); |
| |
| const scopeMappings = |
| await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffsetWithSourceMap(sourceMap, l)); |
| |
| const text = new TextUtils.Text.Text(originalSource); |
| assert.lengthOf(scopeMappings, 1); |
| assert.strictEqual( |
| scopeMappings[0].scopeStart, |
| text.offsetFromPosition(expectedOffsets[0].startLine, expectedOffsets[0].startColumn)); |
| assert.strictEqual( |
| scopeMappings[0].scopeEnd, text.offsetFromPosition(expectedOffsets[0].endLine, expectedOffsets[0].endColumn)); |
| assert.strictEqual(scopeMappings[0].variableMap.get('par1')?.value, 42); |
| assert.strictEqual(scopeMappings[0].variableMap.get('par2')?.value, 1); |
| }); |
| |
| it('can resolve nested scope mappings with source map', async () => { |
| const sourceMapUrl = 'file:///tmp/example.js.min.map'; |
| // This example was minified with terser v5.7.0 with following command. |
| // 'terser index.js -m --toplevel -o example.min.js --source-map "url=example.min.js.map,includeSources"' |
| const source = |
| `function o(o){const n=console.log.bind(console);for(let c=0;c<o;c++)n(c)}o(10);\n//# sourceMappingURL=${ |
| sourceMapUrl}`; |
| const scopes = |
| ' { < >} '; |
| |
| const originalSource = |
| 'function f(n) {\n const c = console.log.bind(console);\n for (let i = 0; i < n; i++) c(i);\n}\nf(10);\n'; |
| const originalScopes = |
| ' { \n \n < >\n }'; |
| const expectedOffsets = parseScopeChain(originalScopes); |
| |
| const sourceMapContent = { |
| version: 3, |
| names: ['f', 'n', 'c', 'console', 'log', 'bind', 'i'], |
| sources: ['index.js'], |
| sourcesContent: [originalSource], |
| mappings: |
| 'AAAA,SAASA,EAAEC,GACT,MAAMC,EAAIC,QAAQC,IAAIC,KAAKF,SAC3B,IAAK,IAAIG,EAAI,EAAGA,EAAIL,EAAGK,IAAKJ,EAAEI,EAChC,CACAN,EAAE', |
| }; |
| const sourceMapJson = JSON.stringify(sourceMapContent); |
| |
| const functionScopeObject = backend.createSimpleRemoteObject([{name: 'o', value: 10}, {name: 'n', value: 1234}]); |
| const forScopeObject = backend.createSimpleRemoteObject([{name: 'c', value: 5}]); |
| |
| const callFrame = await backend.createCallFrame( |
| target, {url: URL, content: source}, scopes, {url: sourceMapUrl, content: sourceMapJson}, |
| [forScopeObject, functionScopeObject]); |
| |
| // Get source map for mapping locations to 'editor' offsets. |
| const sourceMap = await callFrame.debuggerModel.sourceMapManager().sourceMapForClientPromise(callFrame.script); |
| |
| const scopeMappings = |
| await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffsetWithSourceMap(sourceMap, l)); |
| |
| const text = new TextUtils.Text.Text(originalSource); |
| assert.lengthOf(scopeMappings, 2); |
| assert.strictEqual( |
| scopeMappings[0].scopeStart, |
| text.offsetFromPosition(expectedOffsets[0].startLine, expectedOffsets[0].startColumn)); |
| assert.strictEqual( |
| scopeMappings[0].scopeEnd, text.offsetFromPosition(expectedOffsets[0].endLine, expectedOffsets[0].endColumn)); |
| assert.strictEqual(scopeMappings[0].variableMap.get('i')?.value, 5); |
| assert.strictEqual(scopeMappings[0].variableMap.size, 1); |
| assert.strictEqual( |
| scopeMappings[1].scopeStart, |
| text.offsetFromPosition(expectedOffsets[1].startLine, expectedOffsets[1].startColumn)); |
| assert.strictEqual( |
| scopeMappings[1].scopeEnd, text.offsetFromPosition(expectedOffsets[1].endLine, expectedOffsets[1].endColumn)); |
| assert.strictEqual(scopeMappings[1].variableMap.get('n')?.value, 10); |
| assert.strictEqual(scopeMappings[1].variableMap.get('c')?.value, 1234); |
| assert.strictEqual(scopeMappings[1].variableMap.size, 2); |
| }); |
| |
| it('can resolve simple scope mappings', async () => { |
| const source = 'function f(a) { debugger } f(1)'; |
| const scopes = ' { }'; |
| const expectedOffsets = parseScopeChain(scopes); |
| |
| const functionScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); |
| |
| const callFrame = |
| await backend.createCallFrame(target, {url: URL, content: source}, scopes, null, [functionScopeObject]); |
| |
| const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); |
| |
| assert.lengthOf(scopeMappings, 1); |
| assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); |
| assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); |
| assert.strictEqual(scopeMappings[0].variableMap.get('a')?.value, 1); |
| assert.strictEqual(scopeMappings[0].variableMap.size, 1); |
| }); |
| |
| it('can resolve nested scope mappings for block with no variables', async () => { |
| const source = 'function f() { let a = 1; { debugger } } f()'; |
| const scopes = ' { < > }'; |
| const expectedOffsets = parseScopeChain(scopes); |
| |
| const functionScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); |
| const blockScopeObject = backend.createSimpleRemoteObject([]); |
| |
| const callFrame = await backend.createCallFrame( |
| target, {url: URL, content: source}, scopes, null, [blockScopeObject, functionScopeObject]); |
| |
| const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); |
| |
| assert.lengthOf(scopeMappings, 2); |
| assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); |
| assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); |
| assert.strictEqual(scopeMappings[0].variableMap.size, 0); |
| assert.strictEqual(scopeMappings[1].scopeStart, expectedOffsets[1].startColumn); |
| assert.strictEqual(scopeMappings[1].scopeEnd, expectedOffsets[1].endColumn); |
| assert.strictEqual(scopeMappings[1].variableMap.get('a')?.value, 1); |
| assert.strictEqual(scopeMappings[1].variableMap.size, 1); |
| }); |
| |
| it('can resolve nested scope mappings for function with no variables', async () => { |
| const source = 'function f() { console.log("Hi"); { let a = 1; debugger } } f()'; |
| const scopes = ' { < > }'; |
| const expectedOffsets = parseScopeChain(scopes); |
| |
| const functionScopeObject = backend.createSimpleRemoteObject([]); |
| const blockScopeObject = backend.createSimpleRemoteObject([{name: 'a', value: 1}]); |
| |
| const callFrame = await backend.createCallFrame( |
| target, {url: URL, content: source}, scopes, null, [blockScopeObject, functionScopeObject]); |
| |
| const scopeMappings = await Sources.DebuggerPlugin.computeScopeMappings(callFrame, l => toOffset(source, l)); |
| |
| assert.lengthOf(scopeMappings, 2); |
| assert.strictEqual(scopeMappings[0].scopeStart, expectedOffsets[0].startColumn); |
| assert.strictEqual(scopeMappings[0].scopeEnd, expectedOffsets[0].endColumn); |
| assert.strictEqual(scopeMappings[0].variableMap.size, 1); |
| assert.strictEqual(scopeMappings[0].variableMap.get('a')?.value, 1); |
| assert.strictEqual(scopeMappings[1].scopeStart, expectedOffsets[1].startColumn); |
| assert.strictEqual(scopeMappings[1].scopeEnd, expectedOffsets[1].endColumn); |
| assert.strictEqual(scopeMappings[1].variableMap.size, 0); |
| }); |
| }); |
| |
| function makeState(doc: string, extensions: CodeMirror.Extension = []) { |
| return CodeMirror.EditorState.create({ |
| doc, |
| extensions: [ |
| extensions, |
| TextEditor.Config.baseConfiguration(doc), |
| TextEditor.Config.autocompletion.instance(), |
| ], |
| }); |
| } |
| |
| describeWithEnvironment('Inline variable view parser', () => { |
| it('parses simple identifier', () => { |
| const state = makeState('c', CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 0, 1, 1); |
| assert.deepEqual(variables, [{line: 0, from: 0, id: 'c'}]); |
| }); |
| |
| it('parses simple function', () => { |
| const code = `function f(o) { |
| let a = 1; |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}]); |
| }); |
| |
| it('parses patterns', () => { |
| const code = `function f(o) { |
| let {x: a, y: [b, c]} = {x: o, y: [1, 2]}; |
| console.log(a + b + c); |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [ |
| {line: 0, from: 11, id: 'o'}, |
| {line: 1, from: 30, id: 'a'}, |
| {line: 1, from: 37, id: 'b'}, |
| {line: 1, from: 40, id: 'c'}, |
| {line: 1, from: 50, id: 'o'}, |
| {line: 2, from: 71, id: 'console'}, |
| {line: 2, from: 83, id: 'a'}, |
| {line: 2, from: 87, id: 'b'}, |
| {line: 2, from: 91, id: 'c'}, |
| ]); |
| }); |
| |
| it('parses function with nested block', () => { |
| const code = `function f(o) { |
| let a = 1; |
| { |
| let a = 2; |
| debugger; |
| } |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual( |
| variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 3, from: 53, id: 'a'}]); |
| }); |
| |
| it('parses function variable, ignores shadowing let in sibling block', () => { |
| const code = `function f(o) { |
| let a = 1; |
| { |
| let a = 2; |
| console.log(a); |
| } |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual( |
| variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 68, id: 'console'}]); |
| }); |
| |
| it('parses function variable, ignores shadowing const in sibling block', () => { |
| const code = `function f(o) { |
| let a = 1; |
| { |
| const a = 2; |
| console.log(a); |
| } |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual( |
| variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 70, id: 'console'}]); |
| }); |
| |
| it('parses function variable, ignores shadowing typed const in sibling block', () => { |
| const code = `function f(o) { |
| let a: number = 1; |
| { |
| const a: number = 2; |
| console.log(a); |
| } |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual( |
| variables, [{line: 0, from: 11, id: 'o'}, {line: 1, from: 26, id: 'a'}, {line: 4, from: 86, id: 'console'}]); |
| }); |
| |
| it('parses function variable, reports all vars', () => { |
| const code = `function f(o) { |
| var a = 1; |
| { |
| var a = 2; |
| console.log(a); |
| } |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [ |
| {line: 0, from: 11, id: 'o'}, |
| {line: 1, from: 26, id: 'a'}, |
| {line: 3, from: 53, id: 'a'}, |
| {line: 4, from: 68, id: 'console'}, |
| {line: 4, from: 80, id: 'a'}, |
| ]); |
| }); |
| |
| it('parses function variable, handles shadowing in doubly nested scopes', () => { |
| const code = `function f() { |
| let a = 1; |
| let b = 2; |
| let c = 3; |
| { |
| let b; |
| { |
| const c = 4; |
| b = 5; |
| console.log(c); |
| } |
| console.log(c); |
| } |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [ |
| {line: 1, from: 25, id: 'a'}, |
| {line: 2, from: 42, id: 'b'}, |
| {line: 3, from: 59, id: 'c'}, |
| {line: 9, from: 149, id: 'console'}, |
| {line: 11, from: 183, id: 'console'}, |
| {line: 11, from: 195, id: 'c'}, |
| ]); |
| }); |
| |
| it('parses function variable, handles shadowing with object pattern', () => { |
| const code = `function f() { |
| let a = 1; |
| { |
| let {x: b, y: a} = {x: 1, y: 2}; |
| console.log(a + b); |
| } |
| console.log(a); |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [ |
| {line: 1, from: 25, id: 'a'}, |
| {line: 4, from: 89, id: 'console'}, |
| {line: 6, from: 123, id: 'console'}, |
| {line: 6, from: 135, id: 'a'}, |
| ]); |
| }); |
| |
| it('parses function variable, handles shadowing with array pattern', () => { |
| const code = `function f() { |
| let a = 1; |
| { |
| const [b, a] = [1, 2]; |
| console.log(a + b); |
| } |
| console.log(a); |
| debugger; |
| }`; |
| const state = makeState(code, CodeMirror.javascript.javascriptLanguage); |
| const variables = Sources.DebuggerPlugin.getVariableNamesByLine(state, 10, code.length, code.indexOf('debugger')); |
| assert.deepEqual(variables, [ |
| {line: 1, from: 25, id: 'a'}, |
| {line: 4, from: 79, id: 'console'}, |
| {line: 6, from: 113, id: 'console'}, |
| {line: 6, from: 125, id: 'a'}, |
| ]); |
| }); |
| }); |
| |
| describeWithEnvironment('Inline variable view scope value resolution', () => { |
| it('resolves single variable in single scope', () => { |
| const value42 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 42} as SDK.RemoteObject.RemoteObject; |
| const scopeMappings = [{scopeStart: 0, scopeEnd: 10, variableMap: new Map([['a', value42]])}]; |
| const variableNames = [{line: 3, from: 5, id: 'a'}]; |
| const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); |
| |
| assert.strictEqual(valuesByLine?.size, 1); |
| assert.strictEqual(valuesByLine?.get(3)?.size, 1); |
| assert.strictEqual(valuesByLine?.get(3)?.get('a')?.value, 42); |
| }); |
| |
| it('resolves shadowed variables', () => { |
| const value1 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 1} as SDK.RemoteObject.RemoteObject; |
| const value2 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 2} as SDK.RemoteObject.RemoteObject; |
| const scopeMappings = [ |
| {scopeStart: 10, scopeEnd: 20, variableMap: new Map([['a', value1]])}, |
| {scopeStart: 0, scopeEnd: 30, variableMap: new Map([['a', value2]])}, |
| ]; |
| const variableNames = [ |
| {line: 0, from: 5, id: 'a'}, // Falls into the outer scope. |
| {line: 10, from: 15, id: 'a'}, // Inner scope. |
| {line: 20, from: 25, id: 'a'}, // Outer scope. |
| {line: 30, from: 35, id: 'a'}, // Outside of any scope. |
| ]; |
| const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); |
| |
| assert.strictEqual(valuesByLine?.size, 3); |
| assert.strictEqual(valuesByLine?.get(0)?.size, 1); |
| assert.strictEqual(valuesByLine?.get(0)?.get('a')?.value, 2); |
| assert.strictEqual(valuesByLine?.get(10)?.size, 1); |
| assert.strictEqual(valuesByLine?.get(10)?.get('a')?.value, 1); |
| assert.strictEqual(valuesByLine?.get(20)?.size, 1); |
| assert.strictEqual(valuesByLine?.get(20)?.get('a')?.value, 2); |
| }); |
| |
| it('resolves multiple variables on the same line', () => { |
| const value1 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 1} as SDK.RemoteObject.RemoteObject; |
| const value2 = {type: Protocol.Runtime.RemoteObjectType.Number, value: 2} as SDK.RemoteObject.RemoteObject; |
| const scopeMappings = [{scopeStart: 10, scopeEnd: 20, variableMap: new Map([['a', value1], ['b', value2]])}]; |
| const variableNames = [ |
| {line: 10, from: 11, id: 'a'}, |
| {line: 10, from: 13, id: 'b'}, |
| {line: 10, from: 15, id: 'a'}, |
| ]; |
| const valuesByLine = Sources.DebuggerPlugin.getVariableValuesByLine(scopeMappings, variableNames); |
| |
| assert.strictEqual(valuesByLine?.size, 1); |
| assert.strictEqual(valuesByLine?.get(10)?.size, 2); |
| assert.strictEqual(valuesByLine?.get(10)?.get('a')?.value, 1); |
| assert.strictEqual(valuesByLine?.get(10)?.get('b')?.value, 2); |
| }); |
| }); |
| |
| describe('DebuggerPlugin', () => { |
| describe('computePopoverHighlightRange', () => { |
| const {computePopoverHighlightRange} = Sources.DebuggerPlugin; |
| |
| it('correctly returns highlight range depending on cursor position and selection', () => { |
| const doc = 'Hello World!'; |
| const selection = CodeMirror.EditorSelection.create([ |
| CodeMirror.EditorSelection.range(2, 5), |
| ]); |
| const state = CodeMirror.EditorState.create({doc, selection}); |
| assert.isNull(computePopoverHighlightRange(state, 'text/plain', 0)); |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/plain', 2), {from: 2, to: 5}); |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/plain', 5), {from: 2, to: 5}); |
| assert.isNull(computePopoverHighlightRange(state, 'text/plain', 10)); |
| assert.isNull(computePopoverHighlightRange(state, 'text/plain', doc.length - 1)); |
| }); |
| |
| describe('in JavaScript files', () => { |
| const extensions = [CodeMirror.javascript.javascript()]; |
| |
| it('correctly returns highlight range for member assignments', () => { |
| const doc = 'obj.foo = 42;'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 0), {from: 0, to: 3}); |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 4), {from: 0, to: 7}); |
| }); |
| |
| it('correctly returns highlight range for member assignments involving `this`', () => { |
| const doc = 'this.x = bar;'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 0), {from: 0, to: 4}); |
| assert.deepInclude(computePopoverHighlightRange(state, 'text/javascript', 5), {from: 0, to: 6}); |
| }); |
| |
| it('correctly reports function calls as potentially side-effecting', () => { |
| const doc = 'getRandomCoffee().name'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('getRandomCoffee')), |
| {containsSideEffects: false}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.lastIndexOf('.')), |
| {containsSideEffects: true}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('name')), |
| {containsSideEffects: true}, |
| ); |
| }); |
| |
| it('correctly reports method calls as potentially side-effecting', () => { |
| const doc = 'utils.getRandomCoffee().name'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('utils')), |
| {containsSideEffects: false}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('getRandomCoffee')), |
| {containsSideEffects: false}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.lastIndexOf('.')), |
| {containsSideEffects: true}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('name')), |
| {containsSideEffects: true}, |
| ); |
| }); |
| |
| it('correctly reports function calls in property accesses as potentially side-effecting', () => { |
| const doc = 'bar[foo()]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('bar')), |
| {containsSideEffects: false, from: 0, to: 'bar'.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correct reports postfix increments in property accesses as potentially side-effecting', () => { |
| const doc = 'a[i++]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correctly reports postfix decrements in property accesses as potentially side-effecting', () => { |
| const doc = 'a[i--]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correctly reports prefix increments in property accesses as potentially side-effecting', () => { |
| const doc = 'array[++index]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correctly reports prefix decrements in property accesses as potentially side-effecting', () => { |
| const doc = 'array[--index]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correctly reports assignment expressions in property accesses as potentially side-effecting', () => { |
| const doc = 'array[index *= 5]'; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf('[')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', doc.indexOf(']')), |
| {containsSideEffects: true, from: 0, to: doc.length}, |
| ); |
| }); |
| |
| it('correctly reports potential side-effects within a larger script', () => { |
| const doc = `var a = new Array(); |
| var i = 0; |
| a[i++]; |
| a[i--]; |
| a[++i]; |
| a[--i]; |
| a[i *= 5]; |
| a[foo()];`; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| |
| for (let offset = 0; (offset = doc.indexOf('a[', offset) + 1) !== 0;) { |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/javascript', offset), |
| {containsSideEffects: true}, |
| ); |
| } |
| }); |
| }); |
| |
| describe('in HTML files', () => { |
| it('correctly returns highlight range for variables in inline <script>s', () => { |
| const doc = `<!DOCTYPE html> |
| <script type="text/javascript"> |
| globalThis.foo = bar + baz; |
| </script>`; |
| const extensions = [CodeMirror.html.html()]; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| for (const name of ['bar', 'baz']) { |
| const from = doc.indexOf(name); |
| const to = from + name.length; |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/html', from), |
| {from, to}, |
| `did not correct highlight '${name}'`, |
| ); |
| } |
| }); |
| |
| it('correctly returns highlight range for variables in inline event handlers', () => { |
| const doc = `<!DOCTYPE html> |
| <button onclick="foo(bar, baz)">Click me!</button>`; |
| const extensions = [CodeMirror.html.html()]; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| for (const name of ['foo', 'bar', 'baz']) { |
| const from = doc.indexOf(name); |
| const to = from + name.length; |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/html', from), |
| {from, to}, |
| `did not correct highlight '${name}'`, |
| ); |
| } |
| }); |
| }); |
| |
| describe('in TSX files', () => { |
| it('correctly returns highlight range for field accesses', () => { |
| const doc = `function foo(obj: any): number { |
| return obj.x + obj.y; |
| }`; |
| const extensions = [CodeMirror.javascript.tsxLanguage]; |
| const state = CodeMirror.EditorState.create({doc, extensions}); |
| for (const name of ['x', 'y']) { |
| const pos = doc.lastIndexOf(name); |
| const from = pos - 4; |
| const to = pos + name.length; |
| assert.deepInclude( |
| computePopoverHighlightRange(state, 'text/typescript-jsx', pos), |
| {from, to}, |
| `did not correct highlight '${name}'`, |
| ); |
| } |
| }); |
| }); |
| }); |
| }); |