From f6aad4ea6c5478ac2ee727ee806e5df2fd9890aa Mon Sep 17 00:00:00 2001 From: Logan Smyth Date: Wed, 25 Apr 2018 12:08:38 -0700 Subject: [PATCH] [scopes] Handle edge-case mappings that come up in TS and other misc bugs (#6092) --- src/actions/pause/mapScopes.js | 2 +- src/test/mochitest/browser.ini | 2 + .../mochitest/browser_dbg-babel-scopes.js | 12 +- .../babel/fixtures/ts-classes/input.ts | 38 + .../babel/fixtures/ts-classes/output.js | 143 ++++ .../babel/fixtures/ts-classes/output.js.map | 1 + .../babel/fixtures/ts-classes/src/mod.ts | 7 + .../mochitest/examples/babel/package.json | 2 + .../mochitest/examples/babel/tsconfig.json | 10 + .../examples/babel/webpack.config.js | 21 +- src/test/mochitest/examples/doc-babel.html | 2 + .../findGeneratedBindingFromPosition.js | 99 ++- src/workers/parser/getScopes/visitor.js | 56 +- .../__snapshots__/getScopes.spec.js.snap | 679 +++++++++++++++++- .../fixtures/scopes/class-declaration.js | 3 + .../parser/tests/fixtures/scopes/ts-sample.ts | 2 + .../tests/fixtures/scopes/tsx-sample.tsx | 10 + src/workers/parser/tests/getScopes.spec.js | 6 + src/workers/parser/tests/helpers/index.js | 2 + src/workers/parser/utils/ast.js | 12 +- 20 files changed, 1027 insertions(+), 82 deletions(-) create mode 100644 src/test/mochitest/examples/babel/fixtures/ts-classes/input.ts create mode 100644 src/test/mochitest/examples/babel/fixtures/ts-classes/output.js create mode 100644 src/test/mochitest/examples/babel/fixtures/ts-classes/output.js.map create mode 100644 src/test/mochitest/examples/babel/fixtures/ts-classes/src/mod.ts create mode 100644 src/test/mochitest/examples/babel/tsconfig.json create mode 100644 src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx diff --git a/src/actions/pause/mapScopes.js b/src/actions/pause/mapScopes.js index 1662939dd1..4c8dbb152f 100644 --- a/src/actions/pause/mapScopes.js +++ b/src/actions/pause/mapScopes.js @@ -251,7 +251,7 @@ function isReliableScope(scope: OriginalScope): boolean { } // As determined by fair dice roll. - return totalBindings === 0 || unknownBindings / totalBindings < 0.9; + return totalBindings === 0 || unknownBindings / totalBindings < 0.1; } function generateClientScope( diff --git a/src/test/mochitest/browser.ini b/src/test/mochitest/browser.ini index 1e5ae415bb..c7d598b9e9 100644 --- a/src/test/mochitest/browser.ini +++ b/src/test/mochitest/browser.ini @@ -7,6 +7,8 @@ support-files = !/devtools/client/commandline/test/helpers.js !/devtools/client/shared/test/shared-head.js examples/babel/polyfill-bundle.js + examples/babel/fixtures/ts-classes/output.js + examples/babel/fixtures/ts-classes/output.js.map examples/babel/fixtures/eval-source-maps/output.js examples/babel/fixtures/eval-source-maps/output.js.map examples/babel/fixtures/for-of/output.js diff --git a/src/test/mochitest/browser_dbg-babel-scopes.js b/src/test/mochitest/browser_dbg-babel-scopes.js index 0695822b0c..dbe305e6be 100644 --- a/src/test/mochitest/browser_dbg-babel-scopes.js +++ b/src/test/mochitest/browser_dbg-babel-scopes.js @@ -7,7 +7,7 @@ requestLongerTimeout(6); // Tests loading sourcemapped sources for Babel's compile output. async function breakpointScopes(dbg, fixture, { line, column }, scopes) { - const filename = `fixtures/${fixture}/input.js`; + const filename = `fixtures/${fixture}/input.`; const fnName = fixture.replace(/-([a-z])/g, (s, c) => c.toUpperCase()); await invokeWithBreakpoint(dbg, fnName, filename, { line, column }, async () => { @@ -22,6 +22,16 @@ add_task(async function() { const dbg = await initDebugger("doc-babel.html"); + await breakpointScopes(dbg, "ts-classes", { line: 36, column: 4 }, [ + "Module", + "AnotherThing()", + ["anyWindow", "Window"], + "AppComponent()", + "decoratorFactory()", + "ExportedOther()", + "fn()", + ]); + await breakpointScopes(dbg, "eval-source-maps", { line: 14, column: 4 }, [ "Block", ["three", "5"], diff --git a/src/test/mochitest/examples/babel/fixtures/ts-classes/input.ts b/src/test/mochitest/examples/babel/fixtures/ts-classes/input.ts new file mode 100644 index 0000000000..cb439a4851 --- /dev/null +++ b/src/test/mochitest/examples/babel/fixtures/ts-classes/input.ts @@ -0,0 +1,38 @@ +// This file essentially reproduces an example Angular component to map testing, +// among other typescript edge cases. + +import { decoratorFactory } from './src/mod.ts'; + +@decoratorFactory({ + selector: 'app-root', +}) +export class AppComponent { + title = 'app'; +} + +const fn = arg => { + console.log("here"); +}; +fn("arg"); + +// Un-decorated exported classes present a mapping challege because +// the class name is mapped to an unhelpful export assignment. +export class ExportedOther { + title = 'app'; +} + +class AnotherThing { + prop = 4; +} + +const anyWindow = (window); +anyWindow.Promise.resolve().then(() => { + anyWindow.tsClasses = function() { + // This file is specifically for testing the mappings of classes and things + // above, which means we don't want to include _other_ references to then. + // To avoid having them be optimized out, we include a no-op eval. + eval(""); + + console.log("pause here"); + }; +}); diff --git a/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js b/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js new file mode 100644 index 0000000000..cb251079f8 --- /dev/null +++ b/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js @@ -0,0 +1,143 @@ +var tsClasses = +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +// This file essentially reproduces an example Angular component to map testing, +// among other typescript edge cases. +var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; +}; +exports.__esModule = true; +var mod_ts_1 = __webpack_require__(1); +var AppComponent = /** @class */ (function () { + function AppComponent() { + this.title = 'app'; + } + AppComponent = __decorate([ + mod_ts_1.decoratorFactory({ + selector: 'app-root' + }) + ], AppComponent); + return AppComponent; +}()); +exports.AppComponent = AppComponent; +var fn = function (arg) { + console.log("here"); +}; +fn("arg"); +// Un-decorated exported classes present a mapping challege because +// the class name is mapped to an unhelpful export assignment. +var ExportedOther = /** @class */ (function () { + function ExportedOther() { + this.title = 'app'; + } + return ExportedOther; +}()); +exports.ExportedOther = ExportedOther; +var AnotherThing = /** @class */ (function () { + function AnotherThing() { + this.prop = 4; + } + return AnotherThing; +}()); +var anyWindow = window; +anyWindow.Promise.resolve().then(function () { + anyWindow.tsClasses = function () { + // This file is specifically for testing the mappings of classes and things + // above, which means we don't want to include _other_ references to then. + // To avoid having them be optimized out, we include a no-op eval. + eval(""); + console.log("pause here"); + }; +}); + + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + +exports.__esModule = true; +function decoratorFactory(opts) { + return function decorator(target) { + return target; + }; +} +exports.decoratorFactory = decoratorFactory; + + +/***/ }) +/******/ ]); +//# sourceMappingURL=output.js.map \ No newline at end of file diff --git a/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js.map b/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js.map new file mode 100644 index 0000000000..8f7b8bb994 --- /dev/null +++ b/src/test/mochitest/examples/babel/fixtures/ts-classes/output.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap a198b304aef989dfadf6","webpack:///./fixtures/ts-classes/input.ts","webpack:///./fixtures/ts-classes/src/mod.ts"],"names":[],"mappings":";;AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAK;AACL;AACA;;AAEA;AACA;AACA;AACA,mCAA2B,0BAA0B,EAAE;AACvD,yCAAiC,eAAe;AAChD;AACA;AACA;;AAEA;AACA,8DAAsD,+DAA+D;;AAErH;AACA;;AAEA;AACA;;;;;;;;;AC7DA,gFAAgF;AAChF,qCAAqC;;;;;;;;AAErC,sCAAgD;AAKhD;IAHA;QAIE,UAAK,GAAG,KAAK,CAAC;IAChB,CAAC;IAFY,YAAY;QAHxB,yBAAgB,CAAC;YAChB,QAAQ,EAAE,UAAU;SACrB,CAAC;OACW,YAAY,CAExB;IAAD,mBAAC;CAAA;AAFY,oCAAY;AAIzB,IAAM,EAAE,GAAG,aAAG;IACZ,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC,CAAC;AACF,EAAE,CAAC,KAAK,CAAC,CAAC;AAEV,mEAAmE;AACnE,8DAA8D;AAC9D;IAAA;QACE,UAAK,GAAG,KAAK,CAAC;IAChB,CAAC;IAAD,oBAAC;AAAD,CAAC;AAFY,sCAAa;AAI1B;IAAA;QACE,SAAI,GAAG,CAAC,CAAC;IACX,CAAC;IAAD,mBAAC;AAAD,CAAC;AAED,IAAM,SAAS,GAAS,MAAO,CAAC;AAChC,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC;IAC/B,SAAS,CAAC,SAAS,GAAG;QACpB,2EAA2E;QAC3E,0EAA0E;QAC1E,kEAAkE;QAClE,IAAI,CAAC,EAAE,CAAC,CAAC;QAET,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IAC5B,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC;;;;;;;;;;ACpCH,0BAAiC,IAA0B;IACzD,OAAO,mBAAmB,MAAM;QAC9B,OAAY,MAAM,CAAC;IACrB,CAAC,CAAC;AACJ,CAAC;AAJD,4CAIC","file":"fixtures/ts-classes/output.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap a198b304aef989dfadf6","// This file essentially reproduces an example Angular component to map testing,\n// among other typescript edge cases.\n\nimport { decoratorFactory } from './src/mod.ts';\n\n@decoratorFactory({\n selector: 'app-root',\n})\nexport class AppComponent {\n title = 'app';\n}\n\nconst fn = arg => {\n console.log(\"here\");\n};\nfn(\"arg\");\n\n// Un-decorated exported classes present a mapping challege because\n// the class name is mapped to an unhelpful export assignment.\nexport class ExportedOther {\n title = 'app';\n}\n\nclass AnotherThing {\n prop = 4;\n}\n\nconst anyWindow = (window);\nanyWindow.Promise.resolve().then(() => {\n anyWindow.tsClasses = function() {\n // This file is specifically for testing the mappings of classes and things\n // above, which means we don't want to include _other_ references to then.\n // To avoid having them be optimized out, we include a no-op eval.\n eval(\"\");\n\n console.log(\"pause here\");\n };\n});\n\n\n\n// WEBPACK FOOTER //\n// ./fixtures/ts-classes/input.ts","\nexport function decoratorFactory(opts: { selector: string }) {\n return function decorator(target) {\n return target;\n };\n}\n\n\n\n\n// WEBPACK FOOTER //\n// ./fixtures/ts-classes/src/mod.ts"],"sourceRoot":""} \ No newline at end of file diff --git a/src/test/mochitest/examples/babel/fixtures/ts-classes/src/mod.ts b/src/test/mochitest/examples/babel/fixtures/ts-classes/src/mod.ts new file mode 100644 index 0000000000..8a394e6561 --- /dev/null +++ b/src/test/mochitest/examples/babel/fixtures/ts-classes/src/mod.ts @@ -0,0 +1,7 @@ + +export function decoratorFactory(opts: { selector: string }) { + return function decorator(target) { + return target; + }; +} + diff --git a/src/test/mochitest/examples/babel/package.json b/src/test/mochitest/examples/babel/package.json index 99cdb693b6..76ee6728eb 100644 --- a/src/test/mochitest/examples/babel/package.json +++ b/src/test/mochitest/examples/babel/package.json @@ -15,6 +15,8 @@ "babel-plugin-add-module-exports": "^0.2.1", "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-preset-env": "^1.6.1", + "ts-loader": "3.5", + "typescript": "^2.8.3", "webpack": "^3.7.1" }, "dependencies": { diff --git a/src/test/mochitest/examples/babel/tsconfig.json b/src/test/mochitest/examples/babel/tsconfig.json new file mode 100644 index 0000000000..1a800b3d24 --- /dev/null +++ b/src/test/mochitest/examples/babel/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "sourceMap": true, + "experimentalDecorators": true + }, + "include": [ + "fixtures/*/input.ts", + "fixtures/*/src/*.ts" + ] +} diff --git a/src/test/mochitest/examples/babel/webpack.config.js b/src/test/mochitest/examples/babel/webpack.config.js index 5f66e90d45..40cfcc65d4 100644 --- a/src/test/mochitest/examples/babel/webpack.config.js +++ b/src/test/mochitest/examples/babel/webpack.config.js @@ -9,10 +9,13 @@ const tests = fs.readdirSync(fixtures).map(name => { const dirname = path.relative(__dirname, path.join(fixtures, name)); + const inputTS = path.join(dirname, "input.ts"); + const inputJS = path.join(dirname, "input.js"); + return { name: _.camelCase(name), dirname, - input: `./${path.join(dirname, "input.js")}`, + input: `./${fs.existsSync(inputTS) ? inputTS : inputJS}`, output: path.join(dirname, "output.js") }; }).filter(Boolean); @@ -68,9 +71,9 @@ module.exports = [ }, devtool, module: { - loaders: babelEnabled - ? [ - { + loaders: [ + babelEnabled + ? { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader", @@ -85,8 +88,14 @@ module.exports = [ ]) } } - ] - : [] + : null, + { + test: /\.tsx?$/, + exclude: /node_modules/, + loader: "ts-loader", + options: {} + } + ].filter(Boolean) } }; }) diff --git a/src/test/mochitest/examples/doc-babel.html b/src/test/mochitest/examples/doc-babel.html index e5c0924ecc..96abdc9434 100644 --- a/src/test/mochitest/examples/doc-babel.html +++ b/src/test/mochitest/examples/doc-babel.html @@ -57,6 +57,8 @@ + + diff --git a/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js b/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js index 751a5137c4..54c8c0de60 100644 --- a/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js +++ b/src/utils/pause/mapScopes/findGeneratedBindingFromPosition.js @@ -56,53 +56,64 @@ export async function findGeneratedBindingFromPosition( generatedRanges ); + let applicableDeclBindings = []; + if (pos.type === "decl") { + const declarationRanges = await getGeneratedLocationRanges( + source, + pos.declaration, + bindingType, + locationType, + sourceMaps + ); + applicableDeclBindings = filterApplicableBindings( + generatedAstBindings, + declarationRanges + ); + } + let result; if (bindingType === "import") { result = await findGeneratedImportReference(applicableBindings); - } else { - result = await findGeneratedReference(applicableBindings); - } - - if (result) { - return result; - } - - if (bindingType === "import" && pos.type === "decl") { - const importName = pos.importName; - if (typeof importName !== "string") { - // Should never happen, just keeping Flow happy. - return null; - } - - let applicableImportBindings = applicableBindings; - if (generatedRanges.length === 0) { - // If the imported name itself does not map to a useful range, fall back - // to resolving the bindinding using the location of the overall - // import declaration. - const importRanges = await getGeneratedLocationRanges( - source, - pos.declaration, - bindingType, - locationType, - sourceMaps - ); - applicableImportBindings = filterApplicableBindings( - generatedAstBindings, - importRanges - ); - if (applicableImportBindings.length === 0) { + if (!result && pos.type === "decl") { + const importName = pos.importName; + if (typeof importName !== "string") { + // Should never happen, just keeping Flow happy. return null; } - } - return await findGeneratedImportDeclaration( - applicableImportBindings, - importName - ); + result = await findGeneratedImportDeclaration( + [ + ...applicableBindings.filter(withLocType("decl")), + ...applicableDeclBindings.filter(withLocType("decl")), + ...applicableBindings.filter(withLocType("ref")), + ...applicableDeclBindings.filter(withLocType("ref")) + ], + importName + ); + } + } else if (pos.type === "decl") { + // If mapping from a declaration, prefer applicable bindings for + // declarations. + result = await findGeneratedReference([ + ...applicableBindings.filter(withLocType("decl")), + ...applicableDeclBindings.filter(withLocType("decl")), + ...applicableBindings.filter(withLocType("ref")), + ...applicableDeclBindings.filter(withLocType("ref")) + ]); + } else { + // If mapping from a reference, prefer applicable binding references. + result = await findGeneratedReference([ + ...applicableBindings.filter(withLocType("ref")), + ...applicableBindings.filter(withLocType("decl")) + ]); } - return null; + return result; +} + +function withLocType(locationType: BindingLocationType) { + return ({ binding: b }) => b.loc.type === locationType; } type ApplicableBinding = { @@ -474,6 +485,18 @@ async function getGeneratedLocationRanges( const ranges = await sourceMaps.getGeneratedRanges(start, source); const resultRanges = ranges.reduce((acc, mapRange) => { + // Some tooling creates ranges that map a line as a whole, which is useful + // for step-debugging, but can easily lead to finding the wrong binding. + // To avoid these false-positives, we entirely ignore ranges that cover + // full lines. + if ( + locationType === "ref" && + mapRange.columnStart === 0 && + mapRange.columnEnd === Infinity + ) { + return acc; + } + const range = { start: { line: mapRange.line, diff --git a/src/workers/parser/getScopes/visitor.js b/src/workers/parser/getScopes/visitor.js index f6c45858a7..825de5b7ea 100644 --- a/src/workers/parser/getScopes/visitor.js +++ b/src/workers/parser/getScopes/visitor.js @@ -449,25 +449,42 @@ const scopeCollectionVisitor = { }; } } else if (t.isClass(node)) { - if (t.isClassDeclaration(node) && t.isIdentifier(node.id)) { - state.declarationBindingIds.add(node.id); - state.scope.bindings[node.id.name] = { - type: "let", - refs: [ - { - type: "decl", - start: fromBabelLocation(node.id.loc.start, state.sourceId), - end: fromBabelLocation(node.id.loc.end, state.sourceId), - declaration: { - start: fromBabelLocation(node.loc.start, state.sourceId), - end: fromBabelLocation(node.loc.end, state.sourceId) - } - } - ] + if (t.isIdentifier(node.id)) { + // For decorated classes, the AST considers the first the decorator + // to be the start of the class. For the purposes of mapping class + // declarations however, we really want to look for the "class Foo" + // piece. To achieve that, we estimate the location of the declaration + // instead. + let declStart = node.loc.start; + if (node.decorators && node.decorators.length > 0) { + // Estimate the location of the "class" keyword since it + // is unlikely to be a different line than the class name. + declStart = { + line: node.id.loc.start.line, + column: node.id.loc.start.column - "class ".length + }; + } + + const declaration = { + start: fromBabelLocation(declStart, state.sourceId), + end: fromBabelLocation(node.loc.end, state.sourceId) }; - } - if (t.isIdentifier(node.id)) { + if (t.isClassDeclaration(node)) { + state.declarationBindingIds.add(node.id); + state.scope.bindings[node.id.name] = { + type: "let", + refs: [ + { + type: "decl", + start: fromBabelLocation(node.id.loc.start, state.sourceId), + end: fromBabelLocation(node.id.loc.end, state.sourceId), + declaration + } + ] + }; + } + const scope = pushTempScope(state, "block", "Class", { start: fromBabelLocation(node.loc.start, state.sourceId), end: fromBabelLocation(node.loc.end, state.sourceId) @@ -481,10 +498,7 @@ const scopeCollectionVisitor = { type: "decl", start: fromBabelLocation(node.id.loc.start, state.sourceId), end: fromBabelLocation(node.id.loc.end, state.sourceId), - declaration: { - start: fromBabelLocation(node.loc.start, state.sourceId), - end: fromBabelLocation(node.loc.end, state.sourceId) - } + declaration } ] }; diff --git a/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap b/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap index ba8e7a875f..f25328e154 100644 --- a/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap +++ b/src/workers/parser/tests/__snapshots__/getScopes.spec.js.snap @@ -1724,6 +1724,36 @@ Array [ ], "type": "let", }, + "Second": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 15, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + }, + "end": Object { + "column": 12, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "let", + }, "this": Object { "refs": Array [], "type": "implicit", @@ -1732,7 +1762,7 @@ Array [ "displayName": "Module", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -1747,7 +1777,7 @@ Array [ "displayName": "Lexical Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -1792,11 +1822,30 @@ Array [ ], "type": "global", }, + "decorator": Object { + "refs": Array [ + Object { + "end": Object { + "column": 10, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 1, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, }, "displayName": "Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -1959,6 +2008,36 @@ Array [ ], "type": "let", }, + "Second": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 15, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + }, + "end": Object { + "column": 12, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "let", + }, "this": Object { "refs": Array [], "type": "implicit", @@ -1967,7 +2046,7 @@ Array [ "displayName": "Module", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -1982,7 +2061,7 @@ Array [ "displayName": "Lexical Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -2027,11 +2106,30 @@ Array [ ], "type": "global", }, + "decorator": Object { + "refs": Array [ + Object { + "end": Object { + "column": 10, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 1, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, }, "displayName": "Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -2279,6 +2377,36 @@ Array [ ], "type": "let", }, + "Second": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 15, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + }, + "end": Object { + "column": 12, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 14, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "let", + }, "this": Object { "refs": Array [], "type": "implicit", @@ -2287,7 +2415,7 @@ Array [ "displayName": "Module", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -2302,7 +2430,7 @@ Array [ "displayName": "Lexical Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -2347,11 +2475,30 @@ Array [ ], "type": "global", }, + "decorator": Object { + "refs": Array [ + Object { + "end": Object { + "column": 10, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 1, + "line": 13, + "sourceId": "scopes/class-declaration/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, }, "displayName": "Global", "end": Object { "column": 0, - "line": 12, + "line": 15, "sourceId": "scopes/class-declaration/originalSource-1", }, "start": Object { @@ -12378,7 +12525,7 @@ Array [ "displayName": "Lexical Global", "end": Object { "column": 0, - "line": 9, + "line": 11, "sourceId": "scopes/ts-sample/originalSource-1", }, "start": Object { @@ -12409,15 +12556,64 @@ Array [ ], "type": "global", }, + "foo": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 22, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + }, + "end": Object { + "column": 7, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "start": Object { + "column": 4, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "var", + }, "this": Object { "refs": Array [], "type": "implicit", }, + "window": Object { + "refs": Array [ + Object { + "end": Object { + "column": 21, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 15, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, }, "displayName": "Global", "end": Object { "column": 0, - "line": 9, + "line": 11, "sourceId": "scopes/ts-sample/originalSource-1", }, "start": Object { @@ -12568,7 +12764,7 @@ Array [ "displayName": "Lexical Global", "end": Object { "column": 0, - "line": 9, + "line": 11, "sourceId": "scopes/ts-sample/originalSource-1", }, "start": Object { @@ -12599,15 +12795,64 @@ Array [ ], "type": "global", }, + "foo": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 22, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + }, + "end": Object { + "column": 7, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "start": Object { + "column": 4, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "var", + }, "this": Object { "refs": Array [], "type": "implicit", }, + "window": Object { + "refs": Array [ + Object { + "end": Object { + "column": 21, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 15, + "line": 10, + "sourceId": "scopes/ts-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, }, "displayName": "Global", "end": Object { "column": 0, - "line": 9, + "line": 11, "sourceId": "scopes/ts-sample/originalSource-1", }, "start": Object { @@ -12620,6 +12865,414 @@ Array [ ] `; +exports[`getScopes finds scope bindings in a typescript-jsx file at line 3 column 0 1`] = ` +Array [ + Object { + "bindings": Object { + "Color": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 29, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 10, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 5, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "const", + }, + "Example": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 1, + "line": 8, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 13, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "let", + }, + }, + "displayName": "Lexical Global", + "end": Object { + "column": 0, + "line": 11, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 1, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "block", + }, + Object { + "bindings": Object { + "Error": Object { + "refs": Array [ + Object { + "end": Object { + "column": 19, + "line": 6, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 14, + "line": 6, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, + "any": Object { + "refs": Array [ + Object { + "end": Object { + "column": 14, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 11, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, + "foo": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 28, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 7, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 4, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "var", + }, + "this": Object { + "refs": Array [], + "type": "implicit", + }, + }, + "displayName": "Global", + "end": Object { + "column": 0, + "line": 11, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 1, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "object", + }, +] +`; + +exports[`getScopes finds scope bindings in a typescript-jsx file at line 6 column 4 1`] = ` +Array [ + Object { + "bindings": Object { + "arguments": Object { + "refs": Array [], + "type": "implicit", + }, + "this": Object { + "refs": Array [], + "type": "implicit", + }, + }, + "displayName": "method", + "end": Object { + "column": 3, + "line": 7, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 2, + "line": 5, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "function", + }, + Object { + "bindings": Object { + "Example": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 1, + "line": 8, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 13, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "const", + }, + }, + "displayName": "Class", + "end": Object { + "column": 1, + "line": 8, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "block", + }, + Object { + "bindings": Object { + "Color": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 29, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 10, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 5, + "line": 2, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "const", + }, + "Example": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 1, + "line": 8, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 13, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 6, + "line": 4, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "let", + }, + }, + "displayName": "Lexical Global", + "end": Object { + "column": 0, + "line": 11, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 1, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "block", + }, + Object { + "bindings": Object { + "Error": Object { + "refs": Array [ + Object { + "end": Object { + "column": 19, + "line": 6, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 14, + "line": 6, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, + "any": Object { + "refs": Array [ + Object { + "end": Object { + "column": 14, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "meta": null, + "start": Object { + "column": 11, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "ref", + }, + ], + "type": "global", + }, + "foo": Object { + "refs": Array [ + Object { + "declaration": Object { + "end": Object { + "column": 28, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + }, + "end": Object { + "column": 7, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 4, + "line": 10, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "decl", + }, + ], + "type": "var", + }, + "this": Object { + "refs": Array [], + "type": "implicit", + }, + }, + "displayName": "Global", + "end": Object { + "column": 0, + "line": 11, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "start": Object { + "column": 0, + "line": 1, + "sourceId": "scopes/tsx-sample/originalSource-1", + }, + "type": "object", + }, +] +`; + exports[`getScopes finds scope bindings with expression metadata at line 2 column 0 1`] = ` Array [ Object { diff --git a/src/workers/parser/tests/fixtures/scopes/class-declaration.js b/src/workers/parser/tests/fixtures/scopes/class-declaration.js index 1ba6c9e0e4..1c2c74dcbc 100644 --- a/src/workers/parser/tests/fixtures/scopes/class-declaration.js +++ b/src/workers/parser/tests/fixtures/scopes/class-declaration.js @@ -9,3 +9,6 @@ class Outer { } } } + +@decorator +class Second {} diff --git a/src/workers/parser/tests/fixtures/scopes/ts-sample.ts b/src/workers/parser/tests/fixtures/scopes/ts-sample.ts index 5f1d4d3055..68eb57db91 100644 --- a/src/workers/parser/tests/fixtures/scopes/ts-sample.ts +++ b/src/workers/parser/tests/fixtures/scopes/ts-sample.ts @@ -6,3 +6,5 @@ class Example { throw new Error(); } } + +var foo = window; diff --git a/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx b/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx new file mode 100644 index 0000000000..cbd845e537 --- /dev/null +++ b/src/workers/parser/tests/fixtures/scopes/tsx-sample.tsx @@ -0,0 +1,10 @@ + +enum Color {Red, Green, Blue} + +class Example { + method(): never { + throw new Error(); + } +} + +var foo = window; diff --git a/src/workers/parser/tests/getScopes.spec.js b/src/workers/parser/tests/getScopes.spec.js index 14523d0f58..a01e2dfcb6 100644 --- a/src/workers/parser/tests/getScopes.spec.js +++ b/src/workers/parser/tests/getScopes.spec.js @@ -33,6 +33,12 @@ cases( type: "ts", locations: [[3, 0], [6, 4]] }, + { + name: "finds scope bindings in a typescript-jsx file", + file: "scopes/tsx-sample", + type: "tsx", + locations: [[3, 0], [6, 4]] + }, { name: "finds scope bindings in a module", file: "scopes/simple-module", diff --git a/src/workers/parser/tests/helpers/index.js b/src/workers/parser/tests/helpers/index.js index 01dd2ea043..3fb15942cd 100644 --- a/src/workers/parser/tests/helpers/index.js +++ b/src/workers/parser/tests/helpers/index.js @@ -15,6 +15,8 @@ export function getSource(name, type = "js") { contentType = "text/html"; } else if (type === "ts") { contentType = "text/typescript"; + } else if (type === "tsx") { + contentType = "text/typescript-jsx"; } return { diff --git a/src/workers/parser/utils/ast.js b/src/workers/parser/utils/ast.js index f5b3a67a11..5b83d91ba1 100644 --- a/src/workers/parser/utils/ast.js +++ b/src/workers/parser/utils/ast.js @@ -82,7 +82,11 @@ export function getAst(sourceId: string) { const { contentType } = source; if (contentType == "text/html") { ast = parseScriptTags(source.text, htmlParser) || {}; - } else if (contentType && contentType.match(/(javascript|jsx)/)) { + } else if ( + contentType && + contentType.match(/(javascript|jsx)/) && + !contentType.match(/typescript-jsx/) + ) { const type = source.id.includes("original") ? "original" : "generated"; const options = sourceOptions[type]; ast = parse(source.text, options); @@ -91,7 +95,11 @@ export function getAst(sourceId: string) { ...sourceOptions.original, plugins: [ ...sourceOptions.original.plugins.filter( - p => p !== "flow" && p !== "decorators" && p !== "decorators2" + p => + p !== "flow" && + p !== "decorators" && + p !== "decorators2" && + (p !== "jsx" || contentType.match(/typescript-jsx/)) ), "decorators", "typescript"