Skip to content
This repository was archived by the owner on Sep 24, 2021. It is now read-only.

Initial commit of Analyzer-based Linter. Ported over first linter 'unbalanced-delimiters' #1

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
{
"name": "polymer-linter",
"version": "1.0.0",
"version": "1.0.0-alpha.1",
"description": "Lint Polymer!",
"main": "lib/polymer-linter.js",
"bin": {
"polymer-linter": "bin/linter-cli"
},
"main": "lib/linter.js",
"dependencies": {
"dom5": "^2.0.0",
"parse5": "^2.2.1",
"polymer-analyzer": "^2.0.0-alpha.11"
"polymer-analyzer": "^2.0.0-alpha.14"
},
"devDependencies": {
"@types/mocha": "^2.2.32",
Expand Down
168 changes: 168 additions & 0 deletions src/expressions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* @license
* Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/

// TODO(usergenic): Migrate this to polymer-analyzer
// https://github.com/Polymer/polymer-analyzer/issues/351

/**
* A parsed polymer binding expression
* @param {Array.<string>} keys The keys referenced by this expression.
* @param {Array.<string>} methods The methods referenced by this expression.
* @param {string} type One of 'computed', 'literal', or 'reference'
* @param {string} raw The unparsed expression
*/
export class ParsedExpression {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend instead making this:

type ParsedExpression = Computed | Literal | Reference

And make each of those three types an interface with a string literal type field. That way we can separate out the fields and type safety will help us in a lot of ways.

This is how we're typing estree, and it's paid off hugely. It lets you switch over expression.type.. lots of stuff.

public keys: Array<string>;
public methods: Array<string>;
public type: string;
public raw: string;
}

export interface Signature {
method: string;
static: boolean;
args?: Argument[];
}

export interface Argument {
name: string;
value?: string|number;
literal?: boolean;
structured?: boolean;
wildcard?: boolean;
}

function primaryName(expression: string): string {
// TODO(usergenic): Remove this commented out section copied in from polylint
// if (expression.name) {
// expression = expression.name;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this was sometimes an estree.Identifier, I say aggressively delete.

// }
if (expression.match(/^('|").*('|")$/)) { // string literal
return '';
}
if (expression.match(/^-?\d*\.?\d+$/)) { // number literal
return '';
}
if (expression.indexOf('!') === 0) {
return primaryName(expression.slice(1));
}
if (expression.indexOf('.') === -1) {
return expression;
} else {
return expression.split('.')[0];
}
}

export class ExpressionParser {
public extractBindingExpression(text: string): string {
const match = text.match(/\{\{(.*)\}\}/) || text.match(/\[\[(.*)\]\]/);
if (match && match.length === 2) {
let expression: string = match[1];
if (expression.indexOf('::') > -1) {
expression = expression.slice(0, expression.indexOf('::'));
}
return expression.trim();
}
return '';
}

public parseExpression(expression: string): ParsedExpression {
const parsed = new ParsedExpression();
parsed.raw = expression;

const unwrapped = this.extractBindingExpression(expression);
const parsedMethod = this._parseMethod(unwrapped);
if (parsedMethod) {
parsed.type = 'computed';
parsed.keys = parsedMethod.args!.map((arg) => primaryName(arg.name));
parsed.methods = [parsedMethod.method];
} else {
parsed.type = 'reference';
parsed.methods = [];
parsed.keys = [primaryName(unwrapped)];
}
return parsed;
}

private _parseMethod(expression: string): Signature|undefined {
const m = expression.match(/(\w*)\((.*)\)/);
if (m) {
const sig: Signature = {method: m[1], static: true};
if (m[2].trim()) {
// replace escaped commas with comma entity, split on un-escaped commas
const args = m[2].replace(/\\,/g, '&comma;').split(',');
return this._parseArgs(args, sig);
} else {
sig.args = [];
return sig;
}
}
}

private _parseArgs(argList: string[], sig: Signature): Signature {
sig.args = argList.map((rawArg) => {
const arg = this._parseArg(rawArg);
if (!arg.literal) {
sig.static = false;
}
return arg;
});
return sig;
}

private _parseArg(rawArg: string): Argument {
// clean up whitespace
const arg =
rawArg
.trim()
// replace comma entity with comma
.replace(/&comma;/g, ',')
// repair extra escape sequences; note only commas strictly need
// escaping, but we allow any other char to be escaped since its
// likely users will do this
.replace(/\\(.)/g, '\$1');
// basic argument descriptor
const a: Argument = {name: arg};
// detect literal value (must be String or Number)
let fc = arg[0];
if (fc >= '0' && fc <= '9') {
fc = '#';
}
switch (fc) {
case '\'':
case '"':
a.value = arg.slice(1, -1);
a.literal = true;
break;
case '#':
a.value = Number(arg);
a.literal = true;
break;
default:
// no-op
}
// if not literal, look for structured path
if (!a.literal) {
// detect structured path (has dots)
a.structured = arg.indexOf('.') > 0;
if (a.structured) {
a.wildcard = (arg.slice(-2) === '.*');
if (a.wildcard) {
a.name = arg.slice(0, -2);
}
}
}
return a;
}
}
25 changes: 18 additions & 7 deletions src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/

'use strict';

import {Analyzer} from 'polymer-analyzer';
import {Document} from 'polymer-analyzer/lib/model/document';
import {FSUrlLoader} from 'polymer-analyzer/lib/url-loader/fs-url-loader';
import {Warning} from 'polymer-analyzer/lib/warning/warning';
import {Warning, WarningCarryingException} from 'polymer-analyzer/lib/warning/warning';
import {Rule} from './rule';

/**
* The Linter is a simple class which groups together a set of Rules and applies
* them to a set of file urls which can be resolved and loaded by the provided
* Analyzer. A default Analyzer is prepared if one is not provided.
*/
export class Linter {
public analyzer: Analyzer;
public rules: Rule[];
Expand All @@ -36,9 +38,18 @@ export class Linter {
public async lint(files: string[]): Promise<Warning[]> {
let warnings: Warning[] = [];
for (const file of files) {
const document: Document = await this.analyzer.analyze(file);
for (const rule of this.rules) {
warnings = warnings.concat(await rule.check(document));
let document: Document;
try {
document = await this.analyzer.analyze(file);
for (const rule of this.rules) {
warnings = warnings.concat(await rule.check(document));
}
} catch (error) {
if (error instanceof WarningCarryingException) {
warnings.push(error.warning);
continue;
}
throw error;
}
}
return warnings;
Expand Down
7 changes: 4 additions & 3 deletions src/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/

'use strict';

import * as dom5 from 'dom5';

const p = dom5.predicates;

export const isDomBindTemplate =
p.AND(p.hasTagName('template'), p.hasAttrValue('is', 'dom-bind'));
export const isDomModuleTemplate = p.AND(
p.parentMatches(p.hasTagName('dom-module')), p.hasTagName('template'));
export const isTemplate = p.hasTagName('template');
export const isTemplateDescendant = p.parentMatches(isTemplate);
3 changes: 0 additions & 3 deletions src/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/

'use strict';

import {Document} from 'polymer-analyzer/lib/model/document';
import {Warning} from 'polymer-analyzer/lib/warning/warning';

Expand Down
23 changes: 23 additions & 0 deletions src/rules/analyzer-warnings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @license
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
* This code may only be used under the BSD style license found at
* http://polymer.github.io/LICENSE.txt
* The complete set of authors may be found at
* http://polymer.github.io/AUTHORS.txt
* The complete set of contributors may be found at
* http://polymer.github.io/CONTRIBUTORS.txt
* Code distributed by Google as part of the polymer project is also
* subject to an additional IP rights grant found at
* http://polymer.github.io/PATENTS.txt
*/
import {Document} from 'polymer-analyzer/lib/model/model';
import {Warning} from 'polymer-analyzer/lib/warning/warning';
import {Rule} from '../rule';

export class AnalyzerWarnings implements Rule {
public async check(document: Document): Promise<Warning[]> {
const warnings: Warning[] = document.getWarnings();
return warnings;
}
}
Loading