2016-02-17 22:04:32 +00:00
/ * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
* Copyright ( c ) Microsoft Corporation . All rights reserved .
* Licensed under the MIT License . See License . txt in the project root for license information .
* -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- * /
2016-03-07 11:52:53 +00:00
2016-02-17 22:04:32 +00:00
import * as ts from 'typescript' ;
2017-01-11 08:00:57 +00:00
import * as Lint from 'tslint' ;
2016-02-17 22:04:32 +00:00
/ * *
* Implementation of the no - unexternalized - strings rule .
* /
export class Rule extends Lint . Rules . AbstractRule {
public apply ( sourceFile : ts.SourceFile ) : Lint . RuleFailure [ ] {
return this . applyWithWalker ( new NoUnexternalizedStringsRuleWalker ( sourceFile , this . getOptions ( ) ) ) ;
}
}
interface Map < V > {
[ key : string ] : V ;
}
interface UnexternalizedStringsOptions {
signatures? : string [ ] ;
messageIndex? : number ;
keyIndex? : number ;
ignores? : string [ ] ;
}
function isStringLiteral ( node : ts.Node ) : node is ts . StringLiteral {
return node && node . kind === ts . SyntaxKind . StringLiteral ;
}
function isObjectLiteral ( node : ts.Node ) : node is ts . ObjectLiteralExpression {
return node && node . kind === ts . SyntaxKind . ObjectLiteralExpression ;
}
function isPropertyAssignment ( node : ts.Node ) : node is ts . PropertyAssignment {
return node && node . kind === ts . SyntaxKind . PropertyAssignment ;
}
interface KeyMessagePair {
key : ts.StringLiteral ;
message : ts.Node ;
}
class NoUnexternalizedStringsRuleWalker extends Lint . RuleWalker {
2017-11-10 06:54:29 +00:00
private static ImportFailureMessage = 'Do not use double quotes for imports.' ;
2017-08-21 14:50:24 +00:00
2016-02-17 22:04:32 +00:00
private static DOUBLE_QUOTE : string = '"' ;
private signatures : Map < boolean > ;
private messageIndex : number ;
private keyIndex : number ;
private ignores : Map < boolean > ;
private usedKeys : Map < KeyMessagePair [ ] > ;
constructor ( file : ts.SourceFile , opts : Lint.IOptions ) {
super ( file , opts ) ;
this . signatures = Object . create ( null ) ;
this . ignores = Object . create ( null ) ;
this . messageIndex = undefined ;
this . keyIndex = undefined ;
this . usedKeys = Object . create ( null ) ;
let options : any [ ] = this . getOptions ( ) ;
let first : UnexternalizedStringsOptions = options && options . length > 0 ? options [ 0 ] : null ;
if ( first ) {
if ( Array . isArray ( first . signatures ) ) {
first . signatures . forEach ( ( signature : string ) = > this . signatures [ signature ] = true ) ;
}
if ( Array . isArray ( first . ignores ) ) {
first . ignores . forEach ( ( ignore : string ) = > this . ignores [ ignore ] = true ) ;
}
if ( typeof first . messageIndex !== 'undefined' ) {
this . messageIndex = first . messageIndex ;
}
if ( typeof first . keyIndex !== 'undefined' ) {
this . keyIndex = first . keyIndex ;
}
}
}
protected visitSourceFile ( node : ts.SourceFile ) : void {
super . visitSourceFile ( node ) ;
Object . keys ( this . usedKeys ) . forEach ( key = > {
2017-06-12 22:05:57 +00:00
let occurrences = this . usedKeys [ key ] ;
if ( occurrences . length > 1 ) {
occurrences . forEach ( occurrence = > {
this . addFailure ( ( this . createFailure ( occurrence . key . getStart ( ) , occurrence . key . getWidth ( ) , ` Duplicate key ${ occurrence . key . getText ( ) } with different message value. ` ) ) ) ;
2016-02-17 22:04:32 +00:00
} ) ;
}
} ) ;
}
protected visitStringLiteral ( node : ts.StringLiteral ) : void {
this . checkStringLiteral ( node ) ;
super . visitStringLiteral ( node ) ;
}
private checkStringLiteral ( node : ts.StringLiteral ) : void {
let text = node . getText ( ) ;
let doubleQuoted = text . length >= 2 && text [ 0 ] === NoUnexternalizedStringsRuleWalker . DOUBLE_QUOTE && text [ text . length - 1 ] === NoUnexternalizedStringsRuleWalker . DOUBLE_QUOTE ;
let info = this . findDescribingParent ( node ) ;
// Ignore strings in import and export nodes.
2017-08-21 14:50:24 +00:00
if ( info && info . isImport && doubleQuoted ) {
2017-11-07 10:53:59 +00:00
const fix = [
Lint . Replacement . replaceFromTo ( node . getStart ( ) , 1 , '\'' ) ,
Lint . Replacement . replaceFromTo ( node . getStart ( ) + text . length - 1 , 1 , '\'' ) ,
] ;
2017-08-21 14:50:24 +00:00
this . addFailureAtNode (
node ,
NoUnexternalizedStringsRuleWalker . ImportFailureMessage ,
2017-11-07 10:53:59 +00:00
fix
2017-08-21 14:50:24 +00:00
) ;
2016-02-17 22:04:32 +00:00
return ;
}
let callInfo = info ? info.callInfo : null ;
let functionName = callInfo ? callInfo . callExpression . expression . getText ( ) : null ;
if ( functionName && this . ignores [ functionName ] ) {
return ;
}
2016-12-01 08:28:54 +00:00
2016-02-17 22:04:32 +00:00
if ( doubleQuoted && ( ! callInfo || callInfo . argIndex === - 1 || ! this . signatures [ functionName ] ) ) {
2016-12-01 08:28:54 +00:00
const s = node . getText ( ) ;
2017-11-07 10:53:59 +00:00
const fix = [
Lint . Replacement . replaceFromTo ( node . getStart ( ) , node . getWidth ( ) , ` nls.localize('KEY- ${ s . substring ( 1 , s . length - 1 ) } ', ${ s } ) ` ) ,
] ;
2016-12-01 08:28:54 +00:00
this . addFailure ( this . createFailure ( node . getStart ( ) , node . getWidth ( ) , ` Unexternalized string found: ${ node . getText ( ) } ` , fix ) ) ;
2016-02-17 22:04:32 +00:00
return ;
}
// We have a single quoted string outside a localize function name.
if ( ! doubleQuoted && ! this . signatures [ functionName ] ) {
return ;
}
// We have a string that is a direct argument into the localize call.
let keyArg : ts.Expression = callInfo . argIndex === this . keyIndex
? callInfo . callExpression . arguments [ this . keyIndex ]
: null ;
if ( keyArg ) {
if ( isStringLiteral ( keyArg ) ) {
this . recordKey ( keyArg , this . messageIndex ? callInfo . callExpression . arguments [ this . messageIndex ] : undefined ) ;
} else if ( isObjectLiteral ( keyArg ) ) {
for ( let i = 0 ; i < keyArg . properties . length ; i ++ ) {
let property = keyArg . properties [ i ] ;
if ( isPropertyAssignment ( property ) ) {
let name = property . name . getText ( ) ;
if ( name === 'key' ) {
let initializer = property . initializer ;
if ( isStringLiteral ( initializer ) ) {
this . recordKey ( initializer , this . messageIndex ? callInfo . callExpression . arguments [ this . messageIndex ] : undefined ) ;
}
break ;
}
}
}
}
}
2017-11-13 10:12:56 +00:00
const messageArg = callInfo . callExpression . arguments [ this . messageIndex ] ;
if ( messageArg && messageArg . kind !== ts . SyntaxKind . StringLiteral ) {
2016-02-17 22:04:32 +00:00
this . addFailure ( this . createFailure (
messageArg . getStart ( ) , messageArg . getWidth ( ) ,
` Message argument to ' ${ callInfo . callExpression . expression . getText ( ) } ' must be a string literal. ` ) ) ;
return ;
}
}
private recordKey ( keyNode : ts.StringLiteral , messageNode : ts.Node ) {
let text = keyNode . getText ( ) ;
2017-12-06 14:15:45 +00:00
// We have an empty key
if ( text . match ( /(['"]) *\1/ ) ) {
if ( messageNode ) {
this . addFailureAtNode ( keyNode , ` Key is empty for message: ${ messageNode . getText ( ) } ` ) ;
} else {
this . addFailureAtNode ( keyNode , ` Key is empty. ` ) ;
}
return ;
}
2017-06-12 22:05:57 +00:00
let occurrences : KeyMessagePair [ ] = this . usedKeys [ text ] ;
if ( ! occurrences ) {
occurrences = [ ] ;
this . usedKeys [ text ] = occurrences ;
2016-02-17 22:04:32 +00:00
}
if ( messageNode ) {
2017-06-12 22:05:57 +00:00
if ( occurrences . some ( pair = > pair . message ? pair . message . getText ( ) === messageNode . getText ( ) : false ) ) {
2016-02-17 22:04:32 +00:00
return ;
}
}
2017-06-12 22:05:57 +00:00
occurrences . push ( { key : keyNode , message : messageNode } ) ;
2016-02-17 22:04:32 +00:00
}
2017-08-21 14:50:24 +00:00
private findDescribingParent ( node : ts.Node ) : { callInfo ? : { callExpression : ts.CallExpression , argIndex : number } , isImport? : boolean ; } {
2016-02-17 22:04:32 +00:00
let parent : ts.Node ;
while ( ( parent = node . parent ) ) {
let kind = parent . kind ;
if ( kind === ts . SyntaxKind . CallExpression ) {
let callExpression = parent as ts . CallExpression ;
return { callInfo : { callExpression : callExpression , argIndex : callExpression.arguments.indexOf ( < any > node ) } } ;
} else if ( kind === ts . SyntaxKind . ImportEqualsDeclaration || kind === ts . SyntaxKind . ImportDeclaration || kind === ts . SyntaxKind . ExportDeclaration ) {
2017-08-21 14:50:24 +00:00
return { isImport : true } ;
2016-02-17 22:04:32 +00:00
} else if ( kind === ts . SyntaxKind . VariableDeclaration || kind === ts . SyntaxKind . FunctionDeclaration || kind === ts . SyntaxKind . PropertyDeclaration
|| kind === ts . SyntaxKind . MethodDeclaration || kind === ts . SyntaxKind . VariableDeclarationList || kind === ts . SyntaxKind . InterfaceDeclaration
|| kind === ts . SyntaxKind . ClassDeclaration || kind === ts . SyntaxKind . EnumDeclaration || kind === ts . SyntaxKind . ModuleDeclaration
|| kind === ts . SyntaxKind . TypeAliasDeclaration || kind === ts . SyntaxKind . SourceFile ) {
return null ;
}
node = parent ;
}
}
2017-08-21 14:50:24 +00:00
}