summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/utils/TabSources.js
blob: 56e862939fcb3289bb8d05b132b472177ceac63b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const { Ci, Cu } = require("chrome");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { assert, fetch } = DevToolsUtils;
const EventEmitter = require("devtools/shared/event-emitter");
const { OriginalLocation, GeneratedLocation } = require("devtools/server/actors/common");
const { resolve } = require("promise");
const { joinURI } = require("devtools/shared/path");

loader.lazyRequireGetter(this, "SourceActor", "devtools/server/actors/source", true);
loader.lazyRequireGetter(this, "isEvalSource", "devtools/server/actors/source", true);
loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true);
loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true);

/**
 * Manages the sources for a thread. Handles source maps, locations in the
 * sources, etc for ThreadActors.
 */
function TabSources(threadActor, allowSourceFn = () => true) {
  EventEmitter.decorate(this);

  this._thread = threadActor;
  this._useSourceMaps = true;
  this._autoBlackBox = true;
  this._anonSourceMapId = 1;
  this.allowSource = source => {
    return !isHiddenSource(source) && allowSourceFn(source);
  };

  this.blackBoxedSources = new Set();
  this.prettyPrintedSources = new Map();
  this.neverAutoBlackBoxSources = new Set();

  // generated Debugger.Source -> promise of SourceMapConsumer
  this._sourceMaps = new Map();
  // sourceMapURL -> promise of SourceMapConsumer
  this._sourceMapCache = Object.create(null);
  // Debugger.Source -> SourceActor
  this._sourceActors = new Map();
  // url -> SourceActor
  this._sourceMappedSourceActors = Object.create(null);
}

/**
 * Matches strings of the form "foo.min.js" or "foo-min.js", etc. If the regular
 * expression matches, we can be fairly sure that the source is minified, and
 * treat it as such.
 */
const MINIFIED_SOURCE_REGEXP = /\bmin\.js$/;

TabSources.prototype = {
  /**
   * Update preferences and clear out existing sources
   */
  setOptions: function (options) {
    let shouldReset = false;

    if ("useSourceMaps" in options) {
      shouldReset = true;
      this._useSourceMaps = options.useSourceMaps;
    }

    if ("autoBlackBox" in options) {
      shouldReset = true;
      this._autoBlackBox = options.autoBlackBox;
    }

    if (shouldReset) {
      this.reset();
    }
  },

  /**
   * Clear existing sources so they are recreated on the next access.
   *
   * @param Object opts
   *        Specify { sourceMaps: true } if you also want to clear
   *        the source map cache (usually done on reload).
   */
  reset: function (opts = {}) {
    this._sourceActors = new Map();
    this._sourceMaps = new Map();
    this._sourceMappedSourceActors = Object.create(null);

    if (opts.sourceMaps) {
      this._sourceMapCache = Object.create(null);
    }
  },

  /**
   * Return the source actor representing the `source` (or
   * `originalUrl`), creating one if none exists already. May return
   * null if the source is disallowed.
   *
   * @param Debugger.Source source
   *        The source to make an actor for
   * @param String originalUrl
   *        The original source URL of a sourcemapped source
   * @param optional Debguger.Source generatedSource
   *        The generated source that introduced this source via source map,
   *        if any.
   * @param optional String contentType
   *        The content type of the source, if immediately available.
   * @returns a SourceActor representing the source or null.
   */
  source: function ({ source, originalUrl, generatedSource,
                       isInlineSource, contentType }) {
    assert(source || (originalUrl && generatedSource),
           "TabSources.prototype.source needs an originalUrl or a source");

    if (source) {
      // If a source is passed, we are creating an actor for a real
      // source, which may or may not be sourcemapped.

      if (!this.allowSource(source)) {
        return null;
      }

      // It's a hack, but inline HTML scripts each have real sources,
      // but we want to represent all of them as one source as the
      // HTML page. The actor representing this fake HTML source is
      // stored in this array, which always has a URL, so check it
      // first.
      if (source.url in this._sourceMappedSourceActors) {
        return this._sourceMappedSourceActors[source.url];
      }

      if (isInlineSource) {
        // If it's an inline source, the fake HTML source hasn't been
        // created yet (would have returned above), so flip this source
        // into a sourcemapped state by giving it an `originalUrl` which
        // is the HTML url.
        originalUrl = source.url;
        source = null;
      }
      else if (this._sourceActors.has(source)) {
        return this._sourceActors.get(source);
      }
    }
    else if (originalUrl) {
      // Not all "original" scripts are distinctly separate from the
      // generated script. Pretty-printed sources have a sourcemap for
      // themselves, so we need to make sure there a real source
      // doesn't already exist with this URL.
      for (let [source, actor] of this._sourceActors) {
        if (source.url === originalUrl) {
          return actor;
        }
      }

      if (originalUrl in this._sourceMappedSourceActors) {
        return this._sourceMappedSourceActors[originalUrl];
      }
    }

    let actor = new SourceActor({
      thread: this._thread,
      source: source,
      originalUrl: originalUrl,
      generatedSource: generatedSource,
      isInlineSource: isInlineSource,
      contentType: contentType
    });

    let sourceActorStore = this._thread.sourceActorStore;
    var id = sourceActorStore.getReusableActorId(source, originalUrl);
    if (id) {
      actor.actorID = id;
    }

    this._thread.threadLifetimePool.addActor(actor);
    sourceActorStore.setReusableActorId(source, originalUrl, actor.actorID);

    if (this._autoBlackBox &&
        !this.neverAutoBlackBoxSources.has(actor.url) &&
        this._isMinifiedURL(actor.url)) {

      this.blackBox(actor.url);
      this.neverAutoBlackBoxSources.add(actor.url);
    }

    if (source) {
      this._sourceActors.set(source, actor);
    }
    else {
      this._sourceMappedSourceActors[originalUrl] = actor;
    }

    this._emitNewSource(actor);
    return actor;
  },

  _emitNewSource: function (actor) {
    if (!actor.source) {
      // Always notify if we don't have a source because that means
      // it's something that has been sourcemapped, or it represents
      // the HTML file that contains inline sources.
      this.emit("newSource", actor);
    }
    else {
      // If sourcemapping is enabled and a source has sourcemaps, we
      // create `SourceActor` instances for both the original and
      // generated sources. The source actors for the generated
      // sources are only for internal use, however; breakpoints are
      // managed by these internal actors. We only want to notify the
      // user of the original sources though, so if the actor has a
      // `Debugger.Source` instance and a valid source map (meaning
      // it's a generated source), don't send the notification.
      this.fetchSourceMap(actor.source).then(map => {
        if (!map) {
          this.emit("newSource", actor);
        }
      });
    }
  },

  getSourceActor: function (source) {
    if (source.url in this._sourceMappedSourceActors) {
      return this._sourceMappedSourceActors[source.url];
    }

    if (this._sourceActors.has(source)) {
      return this._sourceActors.get(source);
    }

    throw new Error("getSource: could not find source actor for " +
                    (source.url || "source"));
  },

  getSourceActorByURL: function (url) {
    if (url) {
      for (let [source, actor] of this._sourceActors) {
        if (source.url === url) {
          return actor;
        }
      }

      if (url in this._sourceMappedSourceActors) {
        return this._sourceMappedSourceActors[url];
      }
    }

    throw new Error("getSourceActorByURL: could not find source for " + url);
    return null;
  },

  /**
   * Returns true if the URL likely points to a minified resource, false
   * otherwise.
   *
   * @param String aURL
   *        The URL to test.
   * @returns Boolean
   */
  _isMinifiedURL: function (aURL) {
    if (!aURL) {
      return false;
    }

    try {
      let url = new URL(aURL);
      let pathname = url.pathname;
      return MINIFIED_SOURCE_REGEXP.test(pathname.slice(pathname.lastIndexOf("/") + 1));
    } catch (e) {
      // Not a valid URL so don't try to parse out the filename, just test the
      // whole thing with the minified source regexp.
      return MINIFIED_SOURCE_REGEXP.test(aURL);
    }
  },

  /**
   * Create a source actor representing this source. This ignores
   * source mapping and always returns an actor representing this real
   * source. Use `createSourceActors` if you want to respect source maps.
   *
   * @param Debugger.Source aSource
   *        The source instance to create an actor for.
   * @returns SourceActor
   */
  createNonSourceMappedActor: function (aSource) {
    // Don't use getSourceURL because we don't want to consider the
    // displayURL property if it's an eval source. We only want to
    // consider real URLs, otherwise if there is a URL but it's
    // invalid the code below will not set the content type, and we
    // will later try to fetch the contents of the URL to figure out
    // the content type, but it's a made up URL for eval sources.
    let url = isEvalSource(aSource) ? null : aSource.url;
    let spec = { source: aSource };

    // XXX bug 915433: We can't rely on Debugger.Source.prototype.text
    // if the source is an HTML-embedded <script> tag. Since we don't
    // have an API implemented to detect whether this is the case, we
    // need to be conservative and only treat valid js files as real
    // sources. Otherwise, use the `originalUrl` property to treat it
    // as an HTML source that manages multiple inline sources.

    // Assume the source is inline if the element that introduced it is not a
    // script element, or does not have a src attribute.
    let element = aSource.element ? aSource.element.unsafeDereference() : null;
    if (element && (element.tagName !== "SCRIPT" || !element.hasAttribute("src"))) {
      spec.isInlineSource = true;
    } else if (aSource.introductionType === "wasm") {
      // Wasm sources are not JavaScript. Give them their own content-type.
      spec.contentType = "text/wasm";
    } else {
      if (url) {
        // There are a few special URLs that we know are JavaScript:
        // inline `javascript:` and code coming from the console
        if (url.indexOf("Scratchpad/") === 0 ||
            url.indexOf("javascript:") === 0 ||
            url === "debugger eval code") {
          spec.contentType = "text/javascript";
        } else {
          try {
            let pathname = new URL(url).pathname;
            let filename = pathname.slice(pathname.lastIndexOf("/") + 1);
            let index = filename.lastIndexOf(".");
            let extension = index >= 0 ? filename.slice(index + 1) : "";
            if (extension === "xml") {
              // XUL inline scripts may not correctly have the
              // `source.element` property, so do a blunt check here if
              // it's an xml page.
              spec.isInlineSource = true;
            }
            else if (extension === "js") {
              spec.contentType = "text/javascript";
            }
          } catch (e) {
            // This only needs to be here because URL is not yet exposed to
            // workers. (BUG 1258892)
            const filename = url;
            const index = filename.lastIndexOf(".");
            const extension = index >= 0 ? filename.slice(index + 1) : "";
            if (extension === "js") {
              spec.contentType = "text/javascript";
            }
          }
        }
      }
      else {
        // Assume the content is javascript if there's no URL
        spec.contentType = "text/javascript";
      }
    }

    return this.source(spec);
  },

  /**
   * This is an internal function that returns a promise of an array
   * of source actors representing all the source mapped sources of
   * `aSource`, or `null` if the source is not sourcemapped or
   * sourcemapping is disabled. Users should call `createSourceActors`
   * instead of this.
   *
   * @param Debugger.Source aSource
   *        The source instance to create actors for.
   * @return Promise of an array of source actors
   */
  _createSourceMappedActors: function (aSource) {
    if (!this._useSourceMaps || !aSource.sourceMapURL) {
      return resolve(null);
    }

    return this.fetchSourceMap(aSource)
      .then(map => {
        if (map) {
          return map.sources.map(s => {
            return this.source({ originalUrl: s, generatedSource: aSource });
          }).filter(isNotNull);
        }
        return null;
      });
  },

  /**
   * Creates the source actors representing the appropriate sources
   * of `aSource`. If sourcemapped, returns actors for all of the original
   * sources, otherwise returns a 1-element array with the actor for
   * `aSource`.
   *
   * @param Debugger.Source aSource
   *        The source instance to create actors for.
   * @param Promise of an array of source actors
   */
  createSourceActors: function (aSource) {
    return this._createSourceMappedActors(aSource).then(actors => {
      let actor = this.createNonSourceMappedActor(aSource);
      return (actors || [actor]).filter(isNotNull);
    });
  },

  /**
   * Return a promise of a SourceMapConsumer for the source map for
   * `aSource`; if we already have such a promise extant, return that.
   * This will fetch the source map if we don't have a cached object
   * and source maps are enabled (see `_fetchSourceMap`).
   *
   * @param Debugger.Source aSource
   *        The source instance to get sourcemaps for.
   * @return Promise of a SourceMapConsumer
   */
  fetchSourceMap: function (aSource) {
    if (!this._useSourceMaps) {
      return resolve(null);
    }
    else if (this._sourceMaps.has(aSource)) {
      return this._sourceMaps.get(aSource);
    }
    else if (!aSource || !aSource.sourceMapURL) {
      return resolve(null);
    }

    let sourceMapURL = aSource.sourceMapURL;
    if (aSource.url) {
      sourceMapURL = joinURI(aSource.url, sourceMapURL);
    }
    let result = this._fetchSourceMap(sourceMapURL, aSource.url);

    // The promises in `_sourceMaps` must be the exact same instances
    // as returned by `_fetchSourceMap` for `clearSourceMapCache` to
    // work.
    this._sourceMaps.set(aSource, result);
    return result;
  },

  /**
   * Return a promise of a SourceMapConsumer for the source map for
   * `aSource`. The resolved result may be null if the source does not
   * have a source map or source maps are disabled.
   */
  getSourceMap: function (aSource) {
    return resolve(this._sourceMaps.get(aSource));
  },

  /**
   * Set a SourceMapConsumer for the source map for
   * |aSource|.
   */
  setSourceMap: function (aSource, aMap) {
    this._sourceMaps.set(aSource, resolve(aMap));
  },

  /**
   * Return a promise of a SourceMapConsumer for the source map located at
   * |aAbsSourceMapURL|, which must be absolute. If there is already such a
   * promise extant, return it. This will not fetch if source maps are
   * disabled.
   *
   * @param string aAbsSourceMapURL
   *        The source map URL, in absolute form, not relative.
   * @param string aScriptURL
   *        When the source map URL is a data URI, there is no sourceRoot on the
   *        source map, and the source map's sources are relative, we resolve
   *        them from aScriptURL.
   */
  _fetchSourceMap: function (aAbsSourceMapURL, aSourceURL) {
    assert(this._useSourceMaps,
           "Cannot fetch sourcemaps if they are disabled");

    if (this._sourceMapCache[aAbsSourceMapURL]) {
      return this._sourceMapCache[aAbsSourceMapURL];
    }

    let fetching = fetch(aAbsSourceMapURL, { loadFromCache: false })
      .then(({ content }) => {
        let map = new SourceMapConsumer(content);
        this._setSourceMapRoot(map, aAbsSourceMapURL, aSourceURL);
        return map;
      })
      .then(null, error => {
        if (!DevToolsUtils.reportingDisabled) {
          DevToolsUtils.reportException("TabSources.prototype._fetchSourceMap", error);
        }
        return null;
      });
    this._sourceMapCache[aAbsSourceMapURL] = fetching;
    return fetching;
  },

  /**
   * Sets the source map's sourceRoot to be relative to the source map url.
   */
  _setSourceMapRoot: function (aSourceMap, aAbsSourceMapURL, aScriptURL) {
    // No need to do this fiddling if we won't be fetching any sources over the
    // wire.
    if (aSourceMap.hasContentsOfAllSources()) {
      return;
    }

    const base = this._dirname(
      aAbsSourceMapURL.indexOf("data:") === 0
        ? aScriptURL
        : aAbsSourceMapURL);
    aSourceMap.sourceRoot = aSourceMap.sourceRoot
      ? joinURI(base, aSourceMap.sourceRoot)
      : base;
  },

  _dirname: function (aPath) {
    let url = new URL(aPath);
    let href = url.href;
    return href.slice(0, href.lastIndexOf("/"));
  },

  /**
   * Clears the source map cache. Source maps are cached by URL so
   * they can be reused across separate Debugger instances (once in
   * this cache, they will never be reparsed again). They are
   * also cached by Debugger.Source objects for usefulness. By default
   * this just removes the Debugger.Source cache, but you can remove
   * the lower-level URL cache with the `hard` option.
   *
   * @param aSourceMapURL string
   *        The source map URL to uncache
   * @param opts object
   *        An object with the following properties:
   *        - hard: Also remove the lower-level URL cache, which will
   *          make us completely forget about the source map.
   */
  clearSourceMapCache: function (aSourceMapURL, opts = { hard: false }) {
    let oldSm = this._sourceMapCache[aSourceMapURL];

    if (opts.hard) {
      delete this._sourceMapCache[aSourceMapURL];
    }

    if (oldSm) {
      // Clear out the current cache so all sources will get the new one
      for (let [source, sm] of this._sourceMaps.entries()) {
        if (sm === oldSm) {
          this._sourceMaps.delete(source);
        }
      }
    }
  },

  /*
   * Forcefully change the source map of a source, changing the
   * sourceMapURL and installing the source map in the cache. This is
   * necessary to expose changes across Debugger instances
   * (pretty-printing is the use case). Generate a random url if one
   * isn't specified, allowing you to set "anonymous" source maps.
   *
   * @param aSource Debugger.Source
   *        The source to change the sourceMapURL property
   * @param aUrl string
   *        The source map URL (optional)
   * @param aMap SourceMapConsumer
   *        The source map instance
   */
  setSourceMapHard: function (aSource, aUrl, aMap) {
    let url = aUrl;
    if (!url) {
      // This is a littly hacky, but we want to forcefully set a
      // sourcemap regardless of sourcemap settings. We want to
      // literally change the sourceMapURL so that all debuggers will
      // get this and pretty-printing will Just Work (Debugger.Source
      // instances are per-debugger, so we can't key off that). To
      // avoid tons of work serializing the sourcemap into a data url,
      // just make a fake URL and stick the sourcemap there.
      url = "internal://sourcemap" + (this._anonSourceMapId++) + "/";
    }
    aSource.sourceMapURL = url;

    // Forcefully set the sourcemap cache. This will be used even if
    // sourcemaps are disabled.
    this._sourceMapCache[url] = resolve(aMap);
    this.emit("updatedSource", this.getSourceActor(aSource));
  },

  /**
   * Return the non-source-mapped location of the given Debugger.Frame. If the
   * frame does not have a script, the location's properties are all null.
   *
   * @param Debugger.Frame aFrame
   *        The frame whose location we are getting.
   * @returns Object
   *          Returns an object of the form { source, line, column }
   */
  getFrameLocation: function (aFrame) {
    if (!aFrame || !aFrame.script) {
      return new GeneratedLocation();
    }
    let {lineNumber, columnNumber} =
        aFrame.script.getOffsetLocation(aFrame.offset);
    return new GeneratedLocation(
      this.createNonSourceMappedActor(aFrame.script.source),
      lineNumber,
      columnNumber
    );
  },

  /**
   * Returns a promise of the location in the original source if the source is
   * source mapped, otherwise a promise of the same location. This can
   * be called with a source from *any* Debugger instance and we make
   * sure to that it works properly, reusing source maps if already
   * fetched. Use this from any actor that needs sourcemapping.
   */
  getOriginalLocation: function (generatedLocation) {
    let {
      generatedSourceActor,
      generatedLine,
      generatedColumn
    } = generatedLocation;
    let source = generatedSourceActor.source;
    let url = source ? source.url : generatedSourceActor._originalUrl;

    // In certain scenarios the source map may have not been fetched
    // yet (or at least tied to this Debugger.Source instance), so use
    // `fetchSourceMap` instead of `getSourceMap`. This allows this
    // function to be called from anywere (across debuggers) and it
    // should just automatically work.
    return this.fetchSourceMap(source).then(map => {
      if (map) {
        let {
          source: originalUrl,
          line: originalLine,
          column: originalColumn,
          name: originalName
        } = map.originalPositionFor({
          line: generatedLine,
          column: generatedColumn == null ? Infinity : generatedColumn
        });

        // Since the `Debugger.Source` instance may come from a
        // different `Debugger` instance (any actor can call this
        // method), we can't rely on any of the source discovery
        // setup (`_discoverSources`, etc) to have been run yet. So
        // we have to assume that the actor may not already exist,
        // and we might need to create it, so use `source` and give
        // it the required parameters for a sourcemapped source.
        return new OriginalLocation(
          originalUrl ? this.source({
            originalUrl: originalUrl,
            generatedSource: source
          }) : null,
          originalLine,
          originalColumn,
          originalName
        );
      }

      // No source map
      return OriginalLocation.fromGeneratedLocation(generatedLocation);
    });
  },

  getAllGeneratedLocations: function (originalLocation) {
    let {
      originalSourceActor,
      originalLine,
      originalColumn
    } = originalLocation;

    let source = (originalSourceActor.source ||
                  originalSourceActor.generatedSource);

    return this.fetchSourceMap(source).then((map) => {
      if (map) {
        map.computeColumnSpans();

        return map.allGeneratedPositionsFor({
          source: originalSourceActor.url,
          line: originalLine,
          column: originalColumn
        }).map(({ line, column, lastColumn }) => {
          return new GeneratedLocation(
            this.createNonSourceMappedActor(source),
            line,
            column,
            lastColumn
          );
        });
      }

      return [GeneratedLocation.fromOriginalLocation(originalLocation)];
    });
  },


  /**
   * Returns a promise of the location in the generated source corresponding to
   * the original source and line given.
   *
   * When we pass a script S representing generated code to `sourceMap`,
   * above, that returns a promise P. The process of resolving P populates
   * the tables this function uses; thus, it won't know that S's original
   * source URLs map to S until P is resolved.
   */
  getGeneratedLocation: function (originalLocation) {
    let { originalSourceActor } = originalLocation;

    // Both original sources and normal sources could have sourcemaps,
    // because normal sources can be pretty-printed which generates a
    // sourcemap for itself. Check both of the source properties to make it work
    // for both kinds of sources.
    let source = originalSourceActor.source || originalSourceActor.generatedSource;

    // See comment about `fetchSourceMap` in `getOriginalLocation`.
    return this.fetchSourceMap(source).then((map) => {
      if (map) {
        let {
          originalLine,
          originalColumn
        } = originalLocation;

        let {
          line: generatedLine,
          column: generatedColumn
        } = map.generatedPositionFor({
          source: originalSourceActor.url,
          line: originalLine,
          column: originalColumn == null ? 0 : originalColumn,
          bias: SourceMapConsumer.LEAST_UPPER_BOUND
        });

        return new GeneratedLocation(
          this.createNonSourceMappedActor(source),
          generatedLine,
          generatedColumn
        );
      }

      return GeneratedLocation.fromOriginalLocation(originalLocation);
    });
  },

  /**
   * Returns true if URL for the given source is black boxed.
   *
   * @param aURL String
   *        The URL of the source which we are checking whether it is black
   *        boxed or not.
   */
  isBlackBoxed: function (aURL) {
    return this.blackBoxedSources.has(aURL);
  },

  /**
   * Add the given source URL to the set of sources that are black boxed.
   *
   * @param aURL String
   *        The URL of the source which we are black boxing.
   */
  blackBox: function (aURL) {
    this.blackBoxedSources.add(aURL);
  },

  /**
   * Remove the given source URL to the set of sources that are black boxed.
   *
   * @param aURL String
   *        The URL of the source which we are no longer black boxing.
   */
  unblackBox: function (aURL) {
    this.blackBoxedSources.delete(aURL);
  },

  /**
   * Returns true if the given URL is pretty printed.
   *
   * @param aURL String
   *        The URL of the source that might be pretty printed.
   */
  isPrettyPrinted: function (aURL) {
    return this.prettyPrintedSources.has(aURL);
  },

  /**
   * Add the given URL to the set of sources that are pretty printed.
   *
   * @param aURL String
   *        The URL of the source to be pretty printed.
   */
  prettyPrint: function (aURL, aIndent) {
    this.prettyPrintedSources.set(aURL, aIndent);
  },

  /**
   * Return the indent the given URL was pretty printed by.
   */
  prettyPrintIndent: function (aURL) {
    return this.prettyPrintedSources.get(aURL);
  },

  /**
   * Remove the given URL from the set of sources that are pretty printed.
   *
   * @param aURL String
   *        The URL of the source that is no longer pretty printed.
   */
  disablePrettyPrint: function (aURL) {
    this.prettyPrintedSources.delete(aURL);
  },

  iter: function () {
    let actors = Object.keys(this._sourceMappedSourceActors).map(k => {
      return this._sourceMappedSourceActors[k];
    });
    for (let actor of this._sourceActors.values()) {
      if (!this._sourceMaps.has(actor.source)) {
        actors.push(actor);
      }
    }
    return actors;
  }
};

/*
 * Checks if a source should never be displayed to the user because
 * it's either internal or we don't support in the UI yet.
 */
function isHiddenSource(aSource) {
  // Ignore the internal Function.prototype script
  return aSource.text === "() {\n}";
}

/**
 * Returns true if its argument is not null.
 */
function isNotNull(aThing) {
  return aThing !== null;
}

exports.TabSources = TabSources;
exports.isHiddenSource = isHiddenSource;