'use strict' ; import * as nls from 'vscode-nls' ; import { TextDocument } from '../cssLanguageTypes' ; import { CSSDataManager } from '../languageFacts/dataManager' ; import * as languageFacts from '../languageFacts/facts' ; import * as nodes from '../parser/cssNodes' ; import { union } from '../utils/arrays' ; import { LintConfigurationSettings , Rule , Rules , Settings } from './lintRules' ; import calculateBoxModel , { Element } from './lintUtil' ; const localize = nls . loadMessageBundle ( ) ; class NodesByRootMap { public data : { [ name ] : { nodes : nodes . Node [ ] ; names : string [ ] ; } ; } = { } ; public add ( root , name , node ? ) { let 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 , document , settings , cssDataManager , entryFilter ? ) { const visitor = new LintVisitor ( document , settings , cssDataManager ) ; node . acceptVisitor ( visitor ) ; visitor . completeValidations ( ) ; return visitor . getEntries ( entryFilter ) ; } static prefixes = [ '-ms-' , '-moz-' , '-o-' , '-webkit-' , ] ; private warnings : nodes . IMarker [ ] = [ ] ; private settings : LintConfigurationSettings ; private keyframes : NodesByRootMap ; private documentText : string ; private validProperties : { [ name ] : boolean ; } ; private constructor ( document , settings , private cssDataManager ) { this . settings = settings ; this . documentText = document . getText ( ) ; this . keyframes = new NodesByRootMap ( ) ; this . validProperties = { } ; const properties = settings . getSetting ( Settings . ValidProperties ) ; if ( Array . isArray ( properties ) ) { properties . forEach ( ( p ) => { if ( typeof p === 'string' ) { const name = p . trim ( ) . toLowerCase ( ) ; if ( name . length ) { this . validProperties [ name ] = true ; } } } ) ; } } private isValidPropertyDeclaration ( element ) { const propertyName = element . fullPropertyName ; return this . validProperties [ propertyName ] ; } private fetch ( input , s ) { const elements = [ ] ; for ( const curr of input ) { if ( curr . fullPropertyName === s ) { elements . push ( curr ) ; } } return elements ; } private fetchWithValue ( input , s , v ) { const elements = [ ] ; for ( const inputElement of input ) { if ( inputElement . fullPropertyName === s ) { const expression = inputElement . node . getValue ( ) ; if ( expression && this . findValueInExpression ( expression , v ) ) { elements . push ( inputElement ) ; } } } return elements ; } private findValueInExpression ( expression , v ) { let found = false ; expression . accept ( node => { if ( node . type === nodes . NodeType . Identifier && node . matches ( v ) ) { found = true ; } return ! found ; } ) ; return found ; } public getEntries ( filter = ( nodes . Level . Warning | nodes . Level . Error ) ) { return this . warnings . filter ( entry => { return ( entry . getLevel ( ) & filter ) !== 0 ; } ) ; } private addEntry ( node , rule , details ? ) { const entry = new nodes . Marker ( node , rule , this . settings . getRule ( rule ) , details ) ; this . warnings . push ( entry ) ; } private getMissingNames ( expected , actual ) { const expectedClone = expected . slice ( 0 ) ; for ( let i = 0 ; i < actual . length ; i ++ ) { const k = expectedClone . indexOf ( actual [ i ] ) ; if ( k !== - 1 ) { expectedClone [ k ] = null ; } } let result = null ; for ( let i = 0 ; i < expectedClone . length ; i ++ ) { const curr = expectedClone [ i ] ; if ( curr ) { if ( result === null ) { result = localize ( 'namelist.single' , "'{0}'" , curr ) ; } else { result = localize ( 'namelist.concatenated' , "{0}, '{1}'" , result , curr ) ; } } } return result ; } public visitNode ( node ) { switch ( node . type ) { case nodes . NodeType . UnknownAtRule : return this . visitUnknownAtRule ( < nodes . UnknownAtRule > node ) ; case nodes . NodeType . Keyframe : return this . visitKeyframe ( < nodes . Keyframe > 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 ) ; case nodes . NodeType . HexColorValue : return this . visitHexColorValue ( < nodes . HexColorValue > node ) ; case nodes . NodeType . Prio : return this . visitPrio ( node ) ; case nodes . NodeType . IdentifierSelector : return this . visitIdentifierSelector ( node ) ; } return true ; } private completeValidations ( ) { this . validateKeyframes ( ) ; } private visitUnknownAtRule ( node ) { const atRuleName = node . getChild ( 0 ) ; if ( ! atRuleName ) { return false ; } const atDirective = this . cssDataManager . getAtDirective ( atRuleName . getText ( ) ) ; if ( atDirective ) { return false ; } this . addEntry ( atRuleName , Rules . UnknownAtRules , `Unknown at rule ${ atRuleName . getText ( ) }` ) ; return true ; } private visitKeyframe ( node ) { const keyword = node . getKeyword ( ) ; if ( ! keyword ) { return false ; } const text = keyword . getText ( ) ; this . keyframes . add ( node . getName ( ) , text , ( text !== '@keyframes' ) ? keyword : null ) ; return true ; } private validateKeyframes ( ) { const expected = [ '@-webkit-keyframes' , '@-moz-keyframes' , '@-o-keyframes' ] ; for ( const name in this . keyframes . data ) { const actual = this . keyframes . data [ name ] . names ; const needsStandard = ( actual . indexOf ( '@keyframes' ) === - 1 ) ; if ( ! needsStandard && actual . length === 1 ) { continue ; } const missingVendorSpecific = this . getMissingNames ( expected , actual ) ; if ( missingVendorSpecific || needsStandard ) { for ( const node of this . keyframes . data [ name ] . nodes ) { if ( needsStandard ) { const message = localize ( 'keyframes.standardrule.missing' , "Always define standard rule '@keyframes' when defining keyframes." ) ; this . addEntry ( node , Rules . IncludeStandardPropertyWhenUsingVendorPrefix , message ) ; } if ( missingVendorSpecific ) { const message = localize ( 'keyframes.vendorspecific.missing' , "Always include all vendor specific rules: Missing: {0}" , missingVendorSpecific ) ; this . addEntry ( node , Rules . AllVendorPrefixes , message ) ; } } } } return true ; } private visitSimpleSelector ( node ) { const firstChar = this . documentText . charAt ( node . offset ) ; if ( node . length === 1 && firstChar === '*' ) { this . addEntry ( node , Rules . UniversalSelector ) ; } return true ; } private visitIdentifierSelector ( node ) { this . addEntry ( node , Rules . AvoidIdSelector ) ; return true ; } private visitImport ( node ) { this . addEntry ( node , Rules . ImportStatemement ) ; return true ; } private visitRuleSet ( node ) { const declarations = node . getDeclarations ( ) ; if ( ! declarations ) { return false ; } if ( ! declarations . hasChildren ( ) ) { this . addEntry ( node . getSelectors ( ) , Rules . EmptyRuleSet ) ; } const propertyTable = [ ] ; for ( const element of declarations . getChildren ( ) ) { if ( element instanceof nodes . Declaration ) { propertyTable . push ( new Element ( element ) ) ; } } const boxModel = calculateBoxModel ( propertyTable ) ; if ( boxModel . width ) { let properties = [ ] ; if ( boxModel . right . value ) { properties = union ( properties , boxModel . right . properties ) ; } if ( boxModel . left . value ) { properties = union ( properties , boxModel . left . properties ) ; } if ( properties . length !== 0 ) { for ( const item of properties ) { this . addEntry ( item . node , Rules . BewareOfBoxModelSize ) ; } this . addEntry ( boxModel . width . node , Rules . BewareOfBoxModelSize ) ; } } if ( boxModel . height ) { let properties = [ ] ; if ( boxModel . top . value ) { properties = union ( properties , boxModel . top . properties ) ; } if ( boxModel . bottom . value ) { properties = union ( properties , boxModel . bottom . properties ) ; } if ( properties . length !== 0 ) { for ( const item of properties ) { this . addEntry ( item . node , Rules . BewareOfBoxModelSize ) ; } this . addEntry ( boxModel . height . node , Rules . BewareOfBoxModelSize ) ; } } let displayElems = this . fetchWithValue ( propertyTable , 'display' , 'inline-block' ) ; if ( displayElems . length > 0 ) { const elem = this . fetch ( propertyTable , 'float' ) ; for ( let index = 0 ; index < elem . length ; index ++ ) { const node = elem [ index ] . node ; const value = node . getValue ( ) ; if ( value && ! value . matches ( 'none' ) ) { this . addEntry ( node , Rules . PropertyIgnoredDueToDisplay , localize ( 'rule.propertyIgnoredDueToDisplayInlineBlock' , "inline-block is ignored due to the float. If 'float' has a value other than 'none', the box is floated and 'display' is treated as 'block'" ) ) ; } } } displayElems = this . fetchWithValue ( propertyTable , 'display' , 'block' ) ; if ( displayElems . length > 0 ) { const elem = this . fetch ( propertyTable , 'vertical-align' ) ; for ( let index = 0 ; index < elem . length ; index ++ ) { this . addEntry ( elem [ index ] . node , Rules . PropertyIgnoredDueToDisplay , localize ( 'rule.propertyIgnoredDueToDisplayBlock' , "Property is ignored due to the display. With 'display: block', vertical-align should not be used." ) ) ; } } const elements = this . fetch ( propertyTable , 'float' ) ; for ( let index = 0 ; index < elements . length ; index ++ ) { const element = elements [ index ] ; if ( ! this . isValidPropertyDeclaration ( element ) ) { this . addEntry ( element . node , Rules . AvoidFloat ) ; } } for ( let i = 0 ; i < propertyTable . length ; i ++ ) { const element = propertyTable [ i ] ; if ( element . fullPropertyName !== 'background' && ! this . validProperties [ element . fullPropertyName ] ) { const value = element . node . getValue ( ) ; if ( value && this . documentText . charAt ( value . offset ) !== '-' ) { const elements = this . fetch ( propertyTable , element . fullPropertyName ) ; if ( elements . length > 1 ) { for ( let k = 0 ; k < elements . length ; k ++ ) { const value = elements [ k ] . node . getValue ( ) ; if ( value && this . documentText . charAt ( value . offset ) !== '-' && elements [ k ] !== element ) { this . addEntry ( element . node , Rules . DuplicateDeclarations ) ; } } } } } } const isExportBlock = node . getSelectors ( ) . matches ( ":export" ) ; if ( ! isExportBlock ) { const propertiesBySuffix = new NodesByRootMap ( ) ; let containsUnknowns = false ; for ( const element of propertyTable ) { const decl = element . node ; if ( this . isCSSDeclaration ( decl ) ) { let name = element . fullPropertyName ; const firstChar = name . charAt ( 0 ) ; if ( firstChar === '-' ) { if ( name . charAt ( 1 ) !== '-' ) { if ( ! this . cssDataManager . isKnownProperty ( name ) && ! this . validProperties [ name ] ) { this . addEntry ( decl . getProperty ( ) ! , Rules . UnknownVendorSpecificProperty ) ; } const nonPrefixedName = decl . getNonPrefixedPropertyName ( ) ; propertiesBySuffix . add ( nonPrefixedName , name , decl . getProperty ( ) ) ; } } else { const fullName = name ; if ( firstChar === '*' || firstChar === '_' ) { this . addEntry ( decl . getProperty ( ) ! , Rules . IEStarHack ) ; name = name . substr ( 1 ) ; } if ( ! this . cssDataManager . isKnownProperty ( fullName ) && ! this . cssDataManager . isKnownProperty ( name ) ) { if ( ! this . validProperties [ name ] ) { this . addEntry ( decl . getProperty ( ) ! , Rules . UnknownProperty , localize ( 'property.unknownproperty.detailed' , "Unknown property: '{0}'" , decl . getFullPropertyName ( ) ) ) ; } } propertiesBySuffix . add ( name , name , null ) ; } } else { containsUnknowns = true ; } } if ( ! containsUnknowns ) { for ( const suffix in propertiesBySuffix . data ) { const entry = propertiesBySuffix . data [ suffix ] ; const actual = entry . names ; const needsStandard = this . cssDataManager . isStandardProperty ( suffix ) && ( actual . indexOf ( suffix ) === - 1 ) ; if ( ! needsStandard && actual . length === 1 ) { continue ; } const expected = [ ] ; for ( let i = 0 , len = LintVisitor . prefixes . length ; i < len ; i ++ ) { const prefix = LintVisitor . prefixes [ i ] ; if ( this . cssDataManager . isStandardProperty ( prefix + suffix ) ) { expected . push ( prefix + suffix ) ; } } const missingVendorSpecific = this . getMissingNames ( expected , actual ) ; if ( missingVendorSpecific || needsStandard ) { for ( const node of entry . nodes ) { if ( needsStandard ) { const message = localize ( 'property.standard.missing' , "Also define the standard property '{0}' for compatibility" , suffix ) ; this . addEntry ( node , Rules . IncludeStandardPropertyWhenUsingVendorPrefix , message ) ; } if ( missingVendorSpecific ) { const message = localize ( 'property.vendorspecific.missing' , "Always include all vendor specific properties: Missing: {0}" , missingVendorSpecific ) ; this . addEntry ( node , Rules . AllVendorPrefixes , message ) ; } } } } } } return true ; } private visitPrio ( node ) { this . addEntry ( node , Rules . AvoidImportant ) ; return true ; } private visitNumericValue ( node ) { const funcDecl = ( node . findParent ( nodes . NodeType . Function ) as nodes . Function ) ; if ( funcDecl && funcDecl . getName ( ) === 'calc' ) { return true ; } const decl = < nodes . Declaration > node . findParent ( nodes . NodeType . Declaration ) ; if ( decl ) { const declValue = decl . getValue ( ) ; if ( declValue ) { const value = node . getValue ( ) ; if ( ! value . unit || languageFacts . units . length . indexOf ( value . unit . toLowerCase ( ) ) === - 1 ) { return true ; } if ( parseFloat ( value . value ) === 0.0 && ! ! value . unit && ! this . validProperties [ decl . getFullPropertyName ( ) ] ) { this . addEntry ( node , Rules . ZeroWithUnit ) ; } } } return true ; } private visitFontFace ( node ) { const declarations = node . getDeclarations ( ) ; if ( ! declarations ) { return false ; } let definesSrc = false , definesFontFamily = false ; let containsUnknowns = false ; for ( const node of declarations . getChildren ( ) ) { if ( this . isCSSDeclaration ( node ) ) { const name = node . getProperty ( ) ! . getName ( ) . toLowerCase ( ) ; if ( name === 'src' ) { definesSrc = true ; } if ( name === 'font-family' ) { definesFontFamily = true ; } } else { containsUnknowns = true ; } } if ( ! containsUnknowns && ( ! definesSrc || ! definesFontFamily ) ) { this . addEntry ( node , Rules . RequiredPropertiesForFontFace ) ; } return true ; } private isCSSDeclaration ( node ) { if ( node instanceof nodes . Declaration ) { if ( ! ( < nodes . Declaration > node ) . getValue ( ) ) { return false ; } const property = ( < nodes . Declaration > node ) . getProperty ( ) ; if ( ! property ) { return false ; } const identifier = property . getIdentifier ( ) ; if ( ! identifier || identifier . containsInterpolation ( ) ) { return false ; } return true ; } return false ; } private visitHexColorValue ( node ) { const length = node . length ; if ( length !== 9 && length !== 7 && length !== 5 && length !== 4 ) { this . addEntry ( node , Rules . HexColorLength ) ; } return false ; } private visitFunction ( node ) { const fnName = node . getName ( ) . toLowerCase ( ) ; let expectedAttrCount = - 1 ; let 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 , Rules . ArgsInColorFunction ) ; } } return true ; } }