chrome-devtools-frontend
Version:
Chrome DevTools UI
725 lines (641 loc) • 31.4 kB
text/typescript
// Copyright 2022 The Chromium Authors. All rights reserved.
// 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 debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({
forceNew: true,
resourceMapping,
targetManager,
});
Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding});
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 || !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}'`,
);
}
});
});
});
});