mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-26 19:44:25 +01:00
540 lines
17 KiB
TypeScript
540 lines
17 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
'use strict';
|
|
|
|
import languageFacts = require('vs/languages/css/common/services/languageFacts');
|
|
import lintRules = require('vs/languages/css/common/services/lintRules');
|
|
import nodes = require('vs/languages/css/common/parser/cssNodes');
|
|
import nls = require('vs/nls');
|
|
import _level = require('vs/languages/css/common/level');
|
|
|
|
class Element {
|
|
|
|
public name: string;
|
|
public node: nodes.Declaration;
|
|
|
|
constructor(text:string, data:nodes.Declaration){
|
|
this.name = text;
|
|
this.node = data;
|
|
}
|
|
}
|
|
|
|
class NodesByRootMap {
|
|
public data:{[name:string]:{nodes:nodes.Node[];names:string[]}} = {};
|
|
|
|
public add(root:string, name:string, node:nodes.Node) : void {
|
|
var entry = this.data[root];
|
|
if (!entry) {
|
|
entry = { nodes: [], names: []};
|
|
this.data[root] = entry;
|
|
}
|
|
entry.names.push(name);
|
|
if (node) {
|
|
entry.nodes.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
export class LintVisitor implements nodes.IVisitor {
|
|
|
|
static entries(node:nodes.Node, settings: lintRules.IConfigurationSettings):nodes.IMarker[] {
|
|
var visitor = new LintVisitor(settings);
|
|
node.accept(visitor);
|
|
return visitor.getEntries();
|
|
}
|
|
|
|
static prefixes = [
|
|
'-ms-', '-moz-', '-o-', '-webkit-', // Quite common
|
|
// '-xv-', '-atsc-', '-wap-', '-khtml-', 'mso-', 'prince-', '-ah-', '-hp-', '-ro-', '-rim-', '-tc-' // Quite un-common
|
|
];
|
|
|
|
private warnings:nodes.IMarker[] = [];
|
|
private configuration:{ [id:string] : _level.Level };
|
|
|
|
constructor(settings: lintRules.IConfigurationSettings = {}) {
|
|
this.configuration = {};
|
|
for (var ruleKey in lintRules.Rules) {
|
|
var rule = lintRules.Rules[ruleKey];
|
|
var level = settings[rule.id] || _level.toLevel(rule.defaultValue);
|
|
this.configuration[rule.id] = level;
|
|
}
|
|
}
|
|
|
|
private fetch(input: Element[], s:string): Element[] {
|
|
var elements: Element[] = [];
|
|
|
|
for (var i = 0; i < input.length; i++) {
|
|
if (input[i].name.toLowerCase() === s) {
|
|
elements.push(input[i]);
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
private fetchWithValue(input: Element[], s:string, v:string): Element[] {
|
|
var elements: Element[] = [];
|
|
for (var i = 0; i < input.length; i++) {
|
|
if (input[i].name.toLowerCase() === s) {
|
|
var expression = input[i].node.getValue();
|
|
if (expression && this.findValueInExpression(expression, v)) {
|
|
elements.push(input[i]);
|
|
}
|
|
}
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
private findValueInExpression(expression: nodes.Expression, v:string):boolean {
|
|
var found= false;
|
|
expression.accept(function(node) {
|
|
if (node.type === nodes.NodeType.Identifier && node.getText() === v) {
|
|
found= true;
|
|
}
|
|
return !found;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
|
|
private fetchWithin(input: Element[], s:string): Element[] {
|
|
var elements: Element[] = [];
|
|
|
|
for (var i = 0; i < input.length; i++) {
|
|
if (input[i].name.toLowerCase().indexOf(s) >= 0) {
|
|
elements.push(input[i]);
|
|
}
|
|
}
|
|
|
|
return elements;
|
|
}
|
|
|
|
public getEntries(filter:number=(_level.Level.Warning | _level.Level.Error)):nodes.IMarker[] {
|
|
return this.warnings.filter((entry) => {
|
|
return (entry.getLevel() & filter) !== 0;
|
|
});
|
|
}
|
|
|
|
private addEntry(node:nodes.Node, rule:lintRules.Rule, details?:string):void {
|
|
var entry = new nodes.Marker(node, rule, this.configuration[rule.id], details);
|
|
this.warnings.push(entry);
|
|
}
|
|
|
|
private getMissingNames(expected:string[], actual: string[]) : string {
|
|
expected = expected.slice(0); // clone
|
|
for (var i = 0; i < actual.length; i++) {
|
|
var k = expected.indexOf(actual[i]);
|
|
if (k !== -1) {
|
|
expected[k] = null;
|
|
}
|
|
}
|
|
var result: string = null;
|
|
for (var i = 0; i < expected.length; i++) {
|
|
var curr = expected[i];
|
|
if (curr) {
|
|
if (result === null) {
|
|
result = nls.localize('namelist.single', "'{0}'", curr);
|
|
} else {
|
|
result = nls.localize('namelist.concatenated', "{0}, '{1}'", result, curr);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public visitNode(node:nodes.Node):boolean {
|
|
switch (node.type) {
|
|
case nodes.NodeType.Stylesheet:
|
|
return this.visitStylesheet(<nodes.Stylesheet> node);
|
|
case nodes.NodeType.FontFace:
|
|
return this.visitFontFace(<nodes.FontFace> node);
|
|
case nodes.NodeType.Ruleset:
|
|
return this.visitRuleSet(<nodes.RuleSet> node);
|
|
case nodes.NodeType.SimpleSelector:
|
|
return this.visitSimpleSelector(<nodes.SimpleSelector> node);
|
|
case nodes.NodeType.Function:
|
|
return this.visitFunction(<nodes.Function> node);
|
|
case nodes.NodeType.NumericValue:
|
|
return this.visitNumericValue(<nodes.NumericValue> node);
|
|
case nodes.NodeType.Import:
|
|
return this.visitImport(<nodes.Import> node);
|
|
}
|
|
return this.visitUnknownNode(node);
|
|
}
|
|
|
|
private visitStylesheet(node: nodes.Stylesheet):boolean {
|
|
// @keyframe and it's vendor specific alternatives
|
|
// @keyframe should be included
|
|
|
|
var keyframes = new NodesByRootMap();
|
|
node.accept((node) => {
|
|
if (node instanceof nodes.Keyframe) {
|
|
var keyword = (<nodes.Keyframe> node).getKeyword();
|
|
var text = keyword.getText();
|
|
keyframes.add((<nodes.Keyframe> node).getName(), text, (text !== '@keyframes') ? keyword : null);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
var expected = ['@-webkit-keyframes', '@-moz-keyframes', '@-o-keyframes'];
|
|
|
|
|
|
var addVendorSpecificWarnings = (node: nodes.Node) => {
|
|
if (needsStandard) {
|
|
var message = nls.localize('keyframes.standardrule.missing', "Always define standard rule '@keyframes' when defining keyframes.");
|
|
this.addEntry(node, lintRules.Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
|
|
}
|
|
if (missingVendorSpecific) {
|
|
var message = nls.localize('keyframes.vendorspecific.missing', "Always include all vendor specific rules: Missing: {0}", missingVendorSpecific);
|
|
this.addEntry(node, lintRules.Rules.AllVendorPrefixes, message);
|
|
}
|
|
};
|
|
|
|
for (var name in keyframes.data) {
|
|
var actual = keyframes.data[name].names;
|
|
var needsStandard = (actual.indexOf('@keyframes') === -1);
|
|
if (!needsStandard && actual.length === 1) {
|
|
continue; // only the non-vendor specific keyword is used, that's fine, no warning
|
|
}
|
|
|
|
var missingVendorSpecific = this.getMissingNames(expected, actual);
|
|
if (missingVendorSpecific || needsStandard) {
|
|
keyframes.data[name].nodes.forEach(addVendorSpecificWarnings);
|
|
}
|
|
};
|
|
|
|
return true;
|
|
}
|
|
|
|
private visitSimpleSelector(node: nodes.SimpleSelector): boolean {
|
|
|
|
var text = node.getText();
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Lint - The universal selector (*) is known to be slow.
|
|
/////////////////////////////////////////////////////////////
|
|
if (text === '*') {
|
|
this.addEntry(node, lintRules.Rules.UniversalSelector);
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Lint - Avoid id selectors
|
|
/////////////////////////////////////////////////////////////
|
|
if (text.indexOf('#') === 0) {
|
|
this.addEntry(node, lintRules.Rules.AvoidIdSelector);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private visitImport(node: nodes.Import): boolean {
|
|
/////////////////////////////////////////////////////////////
|
|
// Lint - Import statements shouldn't be used, because they aren't offering parallel downloads.
|
|
/////////////////////////////////////////////////////////////
|
|
this.addEntry(node, lintRules.Rules.ImportStatemement);
|
|
return true;
|
|
}
|
|
|
|
private visitRuleSet(node: nodes.RuleSet): boolean {
|
|
/////////////////////////////////////////////////////////////
|
|
// Lint - Don't use empty rulesets.
|
|
/////////////////////////////////////////////////////////////
|
|
var declarations = node.getDeclarations();
|
|
if (!declarations) {
|
|
// syntax error
|
|
return false;
|
|
}
|
|
|
|
|
|
if (!declarations.hasChildren()) {
|
|
this.addEntry(node.getSelectors(), lintRules.Rules.EmptyRuleSet);
|
|
}
|
|
|
|
var self = this;
|
|
var propertyTable: Element[] = [];
|
|
declarations.getChildren().forEach(function(element) {
|
|
if (element instanceof nodes.Declaration) {
|
|
var decl = <nodes.Declaration> element;
|
|
propertyTable.push(new Element(decl.getFullPropertyName(), decl));
|
|
}
|
|
}, this);
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Don't use width or height when using padding or border.
|
|
/////////////////////////////////////////////////////////////
|
|
if ((this.fetch(propertyTable, 'width').length > 0 || this.fetch(propertyTable, 'height').length > 0) && (this.fetchWithin(propertyTable, 'padding').length > 0 || this.fetchWithin(propertyTable, 'border').length > 0)) {
|
|
var elements: Element[] = this.fetch(propertyTable, 'width');
|
|
for (var index = 0; index < elements.length; index++) {
|
|
this.addEntry(elements[index].node, lintRules.Rules.NoWidthOrHeightWhenPaddingOrBorder);
|
|
}
|
|
elements = this.fetch(propertyTable, 'height');
|
|
for (var index = 0; index < elements.length; index++) {
|
|
this.addEntry(elements[index].node, lintRules.Rules.NoWidthOrHeightWhenPaddingOrBorder);
|
|
}
|
|
elements = this.fetchWithin(propertyTable, 'padding');
|
|
for (var index = 0; index < elements.length; index++) {
|
|
this.addEntry(elements[index].node, lintRules.Rules.NoWidthOrHeightWhenPaddingOrBorder);
|
|
}
|
|
elements = this.fetchWithin(propertyTable, 'border');
|
|
for (var index = 0; index < elements.length; index++) {
|
|
this.addEntry(elements[index].node, lintRules.Rules.NoWidthOrHeightWhenPaddingOrBorder);
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Properties ignored due to display
|
|
/////////////////////////////////////////////////////////////
|
|
|
|
// With 'display: inline', the width, height, margin-top, margin-bottom, and float properties have no effect
|
|
var displayElems = this.fetchWithValue(propertyTable, 'display', 'inline');
|
|
if (displayElems.length > 0) {
|
|
[ 'width', 'height', 'margin-top', 'margin-bottom', 'float'].forEach(function(prop) {
|
|
var elem = self.fetch(propertyTable, prop);
|
|
for (var index = 0; index < elem.length; index++) {
|
|
self.addEntry(elem[index].node, lintRules.Rules.PropertyIgnoredDueToDisplay);
|
|
}
|
|
});
|
|
}
|
|
|
|
// With 'display: inline-block', 'float' has no effect
|
|
var displayElems = this.fetchWithValue(propertyTable, 'display', 'inline-block');
|
|
if (displayElems.length > 0) {
|
|
var elem = this.fetch(propertyTable, 'float');
|
|
for (var index = 0; index < elem.length; index++) {
|
|
this.addEntry(elem[index].node, lintRules.Rules.PropertyIgnoredDueToDisplay);
|
|
}
|
|
}
|
|
|
|
// With 'display: block', 'vertical-align' has no effect
|
|
var displayElems = this.fetchWithValue(propertyTable, 'display', 'block');
|
|
if (displayElems.length > 0) {
|
|
var elem = this.fetch(propertyTable, 'vertical-align');
|
|
for (var index = 0; index < elem.length; index++) {
|
|
this.addEntry(elem[index].node, lintRules.Rules.PropertyIgnoredDueToDisplay);
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Don't use !important
|
|
/////////////////////////////////////////////////////////////
|
|
node.accept(function(n:nodes.Node) {
|
|
if (n.type === nodes.NodeType.Prio) {
|
|
self.addEntry(n, lintRules.Rules.AvoidImportant);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Avoid 'float'
|
|
/////////////////////////////////////////////////////////////
|
|
|
|
var elements: Element[] = this.fetch(propertyTable, 'float');
|
|
for (var index = 0; index < elements.length; index++) {
|
|
this.addEntry(elements[index].node, lintRules.Rules.AvoidFloat);
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Don't use duplicate declarations.
|
|
/////////////////////////////////////////////////////////////
|
|
for (var i = 0; i < propertyTable.length; i++) {
|
|
var element = propertyTable[i];
|
|
if (element.name.toLowerCase() !== 'background') {
|
|
var value = element.node.getValue();
|
|
if (value && value.getText()[0] !== '-') {
|
|
var elements = this.fetch(propertyTable, element.name);
|
|
if (elements.length > 1) {
|
|
for (var k = 0; k < elements.length; k++) {
|
|
var value = elements[k].node.getValue();
|
|
if (value && value.getText()[0] !== '-' && elements[k] !== element) {
|
|
this.addEntry(element.node, lintRules.Rules.DuplicateDeclarations);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////
|
|
// Unknown propery & When using a vendor-prefixed gradient, make sure to use them all.
|
|
/////////////////////////////////////////////////////////////
|
|
|
|
var propertiesBySuffix = new NodesByRootMap();
|
|
var containsUnknowns = false;
|
|
|
|
declarations.getChildren().forEach((node) => {
|
|
if (this.isCSSDeclaration(node)) {
|
|
var decl = <nodes.Declaration> node;
|
|
var name = decl.getFullPropertyName();
|
|
var firstChar = name.charAt(0);
|
|
|
|
if (firstChar === '-') {
|
|
if (name.charAt(1) !== '-') { // avoid css variables
|
|
if (!languageFacts.isKnownProperty(name)) {
|
|
this.addEntry(decl.getProperty(), lintRules.Rules.UnknownVendorSpecificProperty);
|
|
}
|
|
var nonPrefixedName = decl.getNonPrefixedPropertyName();
|
|
propertiesBySuffix.add(nonPrefixedName, name, decl.getProperty());
|
|
}
|
|
} else {
|
|
if (firstChar === '*' || firstChar === '_') {
|
|
this.addEntry(decl.getProperty(), lintRules.Rules.IEStarHack);
|
|
name = name.substr(1);
|
|
}
|
|
if (!languageFacts.isKnownProperty(name)) {
|
|
this.addEntry(decl.getProperty(), lintRules.Rules.UnknownProperty);
|
|
}
|
|
propertiesBySuffix.add(name, name, null); // don't pass the node as we don't show errors on the standard
|
|
}
|
|
} else {
|
|
containsUnknowns = true;
|
|
}
|
|
});
|
|
|
|
if (!containsUnknowns) { // don't perform this test if there are
|
|
|
|
var addVendorSpecificWarnings = (node: nodes.Node) => {
|
|
if (needsStandard) {
|
|
var message = nls.localize('property.standard.missing', "Also define the standard property '{0}' for compatibility", suffix);
|
|
this.addEntry(node, lintRules.Rules.IncludeStandardPropertyWhenUsingVendorPrefix, message);
|
|
}
|
|
if (missingVendorSpecific) {
|
|
var message = nls.localize('property.vendorspecific.missing', "Always include all vendor specific properties: Missing: {0}", missingVendorSpecific);
|
|
this.addEntry(node, lintRules.Rules.AllVendorPrefixes, message);
|
|
}
|
|
};
|
|
|
|
|
|
for (var suffix in propertiesBySuffix.data) {
|
|
var entry = propertiesBySuffix.data[suffix];
|
|
var actual = entry.names;
|
|
|
|
var needsStandard = languageFacts.isKnownProperty(suffix) && (actual.indexOf(suffix) === -1);
|
|
if (!needsStandard && actual.length === 1) {
|
|
continue; // only the non-vendor specific rule is used, that's fine, no warning
|
|
}
|
|
|
|
var expected : string[] = [];
|
|
for (var i = 0, len = LintVisitor.prefixes.length; i < len; i++) {
|
|
var prefix = LintVisitor.prefixes[i];
|
|
if (languageFacts.isKnownProperty(prefix + suffix)) {
|
|
expected.push(prefix + suffix);
|
|
}
|
|
}
|
|
|
|
var missingVendorSpecific = this.getMissingNames(expected, actual);
|
|
if (missingVendorSpecific || needsStandard) {
|
|
entry.nodes.forEach(addVendorSpecificWarnings);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return true;
|
|
}
|
|
|
|
private visitNumericValue(node:nodes.NumericValue): boolean {
|
|
/////////////////////////////////////////////////////////////
|
|
// 0 has no following unit
|
|
/////////////////////////////////////////////////////////////
|
|
var value = node.getValue();
|
|
if(value.unit === '%') {
|
|
return true;
|
|
}
|
|
|
|
if(parseFloat(value.value) === 0.0 && !!value.unit) {
|
|
this.addEntry(node, lintRules.Rules.ZeroWithUnit);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private visitFontFace(node: nodes.FontFace): boolean {
|
|
var declarations = node.getDeclarations();
|
|
if (!declarations) {
|
|
// syntax error
|
|
return;
|
|
}
|
|
|
|
var definesSrc = false, definesFontFamily = false;
|
|
var containsUnknowns = false;
|
|
declarations.getChildren().forEach((node) => {
|
|
if (this.isCSSDeclaration(node)) {
|
|
var name = ((<nodes.Declaration> node).getProperty().getName().toLocaleLowerCase());
|
|
if (name === 'src') { definesSrc = true; }
|
|
if (name === 'font-family') { definesFontFamily = true; }
|
|
} else {
|
|
containsUnknowns = true;
|
|
}
|
|
});
|
|
|
|
if (!containsUnknowns && (!definesSrc || !definesFontFamily)) {
|
|
this.addEntry(node, lintRules.Rules.RequiredPropertiesForFontFace);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private isCSSDeclaration(node:nodes.Node): boolean {
|
|
if (node instanceof nodes.Declaration) {
|
|
if (!(<nodes.Declaration> node).getValue()) {
|
|
return false;
|
|
}
|
|
var property = (<nodes.Declaration> node).getProperty();
|
|
if (!property || property.getIdentifier().containsInterpolation()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private visitUnknownNode(node:nodes.Node):boolean {
|
|
|
|
// Rule: #eeff00 or #ef0
|
|
if(node.type === nodes.NodeType.HexColorValue) {
|
|
var text = node.getText();
|
|
if(text.length !== 7 && text.length !== 4) {
|
|
this.addEntry(node, lintRules.Rules.HexColorLength);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private visitFunction(node:nodes.Function):boolean {
|
|
|
|
var fnName = node.getName().toLowerCase(),
|
|
expectedAttrCount = -1,
|
|
actualAttrCount = 0;
|
|
|
|
switch(fnName) {
|
|
case 'rgb(':
|
|
case 'hsl(':
|
|
expectedAttrCount = 3;
|
|
break;
|
|
case 'rgba(':
|
|
case 'hsla(':
|
|
expectedAttrCount = 4;
|
|
break;
|
|
}
|
|
|
|
if(expectedAttrCount !== -1) {
|
|
node.getArguments().accept((n) => {
|
|
if(n instanceof nodes.BinaryExpression) {
|
|
actualAttrCount += 1;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if(actualAttrCount !== expectedAttrCount) {
|
|
this.addEntry(node, lintRules.Rules.ArgsInColorFunction);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|