UNPKG

polymer-lint

Version:
322 lines (296 loc) 9.85 kB
'use strict'; function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } } /** * @typedef {{name: string, args: string[], location: LocationInfo}} Directive * @memberof DirectiveStack */ /** * @typedef {DirectiveStack.Directive[]} DirectiveScope * @memberof DirectiveStack */ // Private methods const onDirective = Symbol('onDirective'); const onEnterScope = Symbol('onEnterScope'); const onLeaveScope = Symbol('onLeaveScope'); const initSnapshots = Symbol('initSnapshots'); // Private properties const snapshots = Symbol('snapshots'); /** * @class DirectiveStack * @extends {Array} * @classdesc * A subclass of Array for tracking linter directives. * * ### The stack * DirectiveStack acts like a stack. Each item in the stack is a * [DirectiveScope]{@link DirectiveStack.DirectiveScope}, which is an array of * {@link DirectiveStack.Directive} objects of the form: * * ```javascript * { name: 'directive-name', * args: [ 'arg1', 'arg2', ... ], * location: { line: 10, col: 20 } * } * ``` * * DirectiveStack listens for events emitted by the {@link SAXParser} given to * its [listenTo]{@link DirectiveStack#listenTo} method for the * `linterDirective`, `enterScope`, and `leaveScope` events. It responds in the following ways: * * * {@link SAXParser.event:linterDirective} - A [Directive]{@link DirectiveStack.Directive} * object is initialized with properties equal to the `name`, `args`, and * `location` arguments given by the event. * * A snapshot is recorded (see [Snapshots](#snapshots)). * * * {@link SAXParser.event:enterScope} - A new, empty * [DirectiveScope]{@link DirectiveStack.DirectiveScope} * is pushed onto the stack. * * * {@link SAXParser.event:leaveScope} - The top DirectiveScope array is * popped from the stack and a snapshot is recorded * (see [Snapshots](#snapshots)). * * ### Example * * Consider the following markup: * * ```text * 1| <foo> * 2| <!-- directive-x grumpy, sleepy --> * 3| Line three * 4| <bar> * 5| <!-- directive-x sneezy --> * 6| <!-- directive-y doc --> * 7| Line 7 * 8| </bar> * 9| </foo> * ``` * * Before the parser parses line 1, the stack looks like this: * * ```text * top|bottom [] * ``` * * The empty [DirectiveScope]{@link DirectiveStack.DirectiveScope} * represents the root scope. It's empty because no directives have been * encountered yet. After the parser parses line 1, the stack looks like this: * * ```text * # After line 1 * top [] * bottom [] * ``` * * The parser entered a new scope (`<foo>`), so a new, empty object has been * pushed onto the stack. On the next line, line 2, the parser encounters a * directive, after which the stack looks like this: * * ```text * # After line 2 * top [ { name: 'directive-x', args: [ grumpy, sleepy ], location: ... } ] * bottom [] * ``` * * Fast-forward to line 7. The parser has entered a new scope (`<bar>`) and * encountered two directive in that scope: another instance of `directive-x`, * and `directive-y`. The stack now looks like this: * * ```text * # After line 6 * top [ { name: 'directive-x', args: [ sneezy ], ... }, * { name: 'directive-y', args: [ doc ], ... } ] * ⋮ [ { name: 'directive-x', args: [ grumpy, sleepy ] } ] * bottom [] * ``` * * Inspecting the stack we can see which directives are "in effect," i.e. which * have been encountered so far in the current scope or its ancestors. We can * see that `directive-x` was encountered once with the arguments `grumpy` and * `sleepy` and once with the argument `sneezy`, and `directive-y` was * encountered once with the argument `doc`. * * DirectiveStack provides a convenience method [getDirectives]{@link DirectiveStack#getDirectives} * that will traverse the stack and return an flat array of the Directives * encountered with the given directive name(s). For example: * * ```javascript * // After line 6 * stack.getDirective('directive-x'); * // => [ { name: 'directive-x', args: ['grumpy', 'sleepy'], ... }, * // { name: 'directive-x', args: ['sneezy'], ... } ] * ``` * * On line 8 the parser leaves the `<bar>` scope, so the top of the stack is * popped off: * * ```text * # After line 8 * top [ { name: 'directive-x', args: [ grumpy, sleepy ] } ] * bottom {} * ``` * * Inspecting the stack now we can see that `directive-y` is no longer "in * effect" because the parser left the scope in which it was encountered. * * ### Snapshots * * Whenever a `linterDirective` or `leaveScope` event is received, the * DirectiveStack will record a "snapshot" of itself along with the * {@link SAXParser.LocationInfo} object given by those events. This allows us * to inspect the state of the stack corresponding to any location. * * **Note:** A snapshot is *not* recorded when an `enterScope` event is * received, because those events do not affect which directives are * "in effect." * * A snapshot can be retrieved using the [snapshotAtLocation]{@link DirectiveStack#snapshotAtLocation} * method. For example, given the following markup: * * ```text * 1| <baz> * 2| <!-- directive-x bashful, dopey --> * 3| <qux> * 4| <!-- directive-y happy --> * 5| Line five * 6| </qux> * 7| Line seven * 8| </baz> * 9| Line nine * ``` * * `snapshotAtLocation` would give us the following results (note the bottom-up * output, since stack is implemented with an Array whose beginning is the * bottom of the stack and whose end is the top): * * ```javascript * stack.snapshotAtLocation({ line: 5, col: 1 }); * // => DirectiveStack [ * // [], * // [ { name: 'directive-x', args: [ 'bashful', 'dopey' ], location: { line: 2, col: 3 } } ], * // [ { name: 'directive-y': args: [ 'happy' ], location: { line: 4, col: 5 } } ] * // ] * * stack.snapshotAtLocation({ line: 7, col: 1 }); * // => DirectiveStack [ * // [], * // [ { name: 'directive-x', args: [ 'bashful', 'dopey' ], location: { line: 2, col: 3 } } ], * // ] * * stack.snapshotAtLocation({ line: 9, col: 1 }); * // => DirectiveStack [ [] ] * ``` */ class DirectiveStack extends Array { /** * @param {Object} options * @param {boolean} [options.snapshot] * If `false`, will not initialize the `snapshots` array (for internal use). */ constructor() { var _ref = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; var _ref$snapshot = _ref.snapshot; let snapshot = _ref$snapshot === undefined ? true : _ref$snapshot; super(); this.push([]); // initialize with root scope if (snapshot) { this[initSnapshots](); } } [initSnapshots]() { // initial snapshot has no directives this[snapshots] = [[{ line: 1, col: 1 }, new this.constructor({ snapshot: false })]]; } /** * Returns the DirectiveScope array on top of the stack without popping it * @return {DirectiveStack.DirectiveScope} */ peek() { return this[this.length - 1]; } /** * Returns a flat array of directives on the stack with the given name(s). If * no arguments are given, all directives are returned. * * @param {...string} directiveNames - The names of the directives to return * @return {DirectiveStack.Directive[]} - The matching directives */ getDirectives() { for (var _len = arguments.length, directiveNames = Array(_len), _key = 0; _key < _len; _key++) { directiveNames[_key] = arguments[_key]; } return this.reduce((directives, scope) => directives.concat(directiveNames.length ? scope.filter(_ref2 => { let name = _ref2.name; return directiveNames.indexOf(name) !== -1; }) : scope), []); } /** * Get the snapshot of the stack nearest to but not after the given location * (see [Snapshots](#snapshots)). * * @param {LocationInfo} location * @return {DirectiveStack} */ snapshotAtLocation(_ref3) { let line = _ref3.line; let col = _ref3.col; const snaps = this[snapshots]; // Find the first snapshot whose subsequent snapshot's line/col position is // greater than the given line/col for (let i = 1, c = snaps.length; i <= c; i++) { const nextSnap = snaps[i]; if (!nextSnap) { // Last snapshot return snaps[i - 1][1]; } const nextSnapLoc = nextSnap[0]; if (line < nextSnapLoc.line || line === nextSnapLoc.line && col < nextSnapLoc.col) { return snaps[i - 1][1]; } } return null; } /** * Binds event listeners to the given SAXParser * * @param {SAXParser} parser * @listens SAXParser.event:linterDirective * @listens SAXParser.event:enterScope * @listens SAXParser.event:leaveScope * @return {void} */ listenTo(parser) { parser.on('linterDirective', this[onDirective].bind(this)); parser.on('enterScope', this[onEnterScope].bind(this)); parser.on('leaveScope', this[onLeaveScope].bind(this)); } /** * Returns a copy of itself * @return {DirectiveStack} */ clone() { return this.constructor.from(this); } /** * Records the state of the stack at this location * @param {LocationInfo} location * @return {void} */ snapshot(location) { this[snapshots].push([location, this.clone()]); } [onDirective](name, args, location) { const top = this.pop(); this.push([].concat(_toConsumableArray(top), [{ name, args, location }])); this.snapshot(location); } [onEnterScope]() { this.push([]); // push object for new scope } [onLeaveScope](location) { this.pop(); this.snapshot(location); } } module.exports = DirectiveStack;