eslint-plugin-typescript
Advanced tools
@@ -32,6 +32,12 @@ /** | ||
| function getMemberName(member) { | ||
| if (!member) return null; | ||
| switch (member.type) { | ||
| case "ExportDefaultDeclaration": | ||
| case "ExportNamedDeclaration": { | ||
| return getMemberName(member.declaration); | ||
| // export statements (e.g. export { a };) | ||
| // have no declarations, so ignore them | ||
| return member.declaration | ||
| ? getMemberName(member.declaration) | ||
| : null; | ||
| } | ||
@@ -88,3 +94,3 @@ case "DeclareFunction": | ||
| }); | ||
| } else if (index === -1) { | ||
| } else if (name && index === -1) { | ||
| seen.push(name); | ||
@@ -91,0 +97,0 @@ } |
@@ -53,3 +53,3 @@ /** | ||
| function isTypeScriptModuleDeclaration(node) { | ||
| return node.name && node.name.type === "Literal"; | ||
| return node.id && node.id.type === "Literal"; | ||
| } | ||
@@ -64,7 +64,5 @@ | ||
| function isDeclaration(node) { | ||
| const hasDeclareModifier = | ||
| (node.modifiers || []) | ||
| .filter(m => m.type === "TSDeclareKeyword").length > 0; | ||
| return hasDeclareModifier && !isTypeScriptModuleDeclaration(node); | ||
| return ( | ||
| node.declare === true && !isTypeScriptModuleDeclaration(node) | ||
| ); | ||
| } | ||
@@ -71,0 +69,0 @@ |
+167
-76
@@ -63,3 +63,45 @@ /** | ||
| /** | ||
| * Checks if the given node has any decorators and marks them as used. | ||
| * Checks the given node type annotation and marks it as used. | ||
| * @param {ASTNode} node the relevant AST node. | ||
| * @returns {void} | ||
| * @private | ||
| */ | ||
| function markTypeAnnotationAsUsed(node) { | ||
| const annotation = node.typeAnnotation || node; | ||
| switch (annotation.type) { | ||
| case "TSTypeReference": { | ||
| if (annotation.typeName.type === "TSArrayType") { | ||
| markTypeAnnotationAsUsed( | ||
| annotation.typeName.elementType | ||
| ); | ||
| } else { | ||
| markVariableAsUsed(context, annotation.typeName.name); | ||
| if ( | ||
| annotation.typeParameters && | ||
| annotation.typeParameters.params | ||
| ) { | ||
| annotation.typeParameters.params.forEach(param => { | ||
| markTypeAnnotationAsUsed(param); | ||
| }); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| case "TSUnionType": | ||
| case "TSIntersectionType": | ||
| annotation.types.forEach(type => { | ||
| markTypeAnnotationAsUsed(type); | ||
| }); | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given decorator and marks it as used. | ||
| * @param {ASTNode} node The relevant AST node. | ||
@@ -69,42 +111,35 @@ * @returns {void} | ||
| */ | ||
| function markDecoratorsAsUsed(node) { | ||
| if (!node.decorators || !node.decorators.length) { | ||
| function markDecoratorAsUsed(node) { | ||
| /** | ||
| * Decorator | ||
| */ | ||
| if (node.name) { | ||
| markVariableAsUsed(context, node.name); | ||
| return; | ||
| } | ||
| node.decorators.forEach(decorator => { | ||
| /** | ||
| * Decorator | ||
| */ | ||
| if (decorator.name) { | ||
| markVariableAsUsed(context, decorator.name); | ||
| return; | ||
| } | ||
| if (decorator.expression && decorator.expression.name) { | ||
| markVariableAsUsed(context, decorator.expression.name); | ||
| return; | ||
| } | ||
| if (node.expression && node.expression.name) { | ||
| markVariableAsUsed(context, node.expression.name); | ||
| return; | ||
| } | ||
| /** | ||
| * Decorator Factory | ||
| */ | ||
| if (decorator.callee && decorator.callee.name) { | ||
| markVariableAsUsed(context, decorator.callee.name); | ||
| } | ||
| /** | ||
| * Decorator Factory | ||
| */ | ||
| if (node.callee && node.callee.name) { | ||
| markVariableAsUsed(context, node.callee.name); | ||
| } | ||
| if ( | ||
| decorator.expression && | ||
| decorator.expression.callee && | ||
| decorator.expression.callee.name | ||
| ) { | ||
| markVariableAsUsed( | ||
| context, | ||
| decorator.expression.callee.name | ||
| ); | ||
| } | ||
| }); | ||
| if ( | ||
| node.expression && | ||
| node.expression.callee && | ||
| node.expression.callee.name | ||
| ) { | ||
| markVariableAsUsed(context, node.expression.callee.name); | ||
| } | ||
| } | ||
| /** | ||
| * Checks if the given node has any implemented interfaces and marks them as used. | ||
| * Checks the given interface and marks it as used. | ||
| * Generic arguments are also included in the check. | ||
| * @param {ASTNode} node The relevant AST node. | ||
@@ -114,18 +149,81 @@ * @returns {void} | ||
| */ | ||
| function markImplementedInterfacesAsUsed(node) { | ||
| if (!node.implements || !node.implements.length) { | ||
| function markImplementedInterfaceAsUsed(node) { | ||
| if (!node || !node.id || !node.id.name) { | ||
| return; | ||
| } | ||
| node.implements.forEach(implementedInterface => { | ||
| if ( | ||
| !implementedInterface || | ||
| !implementedInterface.id || | ||
| !implementedInterface.id.name | ||
| ) { | ||
| return; | ||
| } | ||
| markVariableAsUsed(context, implementedInterface.id.name); | ||
| }); | ||
| markVariableAsUsed(context, node.id.name); | ||
| if (!node.typeParameters || !node.typeParameters.params) { | ||
| return; | ||
| } | ||
| node.typeParameters.params.forEach(markTypeAnnotationAsUsed); | ||
| } | ||
| /** | ||
| * Checks the given class has a super class and marks it as used. | ||
| * Generic arguments are also included in the check. | ||
| * @param {ASTNode} node The relevant AST node. | ||
| * @returns {void} | ||
| * @private | ||
| */ | ||
| function markSuperClassAsUsed(node) { | ||
| if (!node.superClass) { | ||
| return; | ||
| } | ||
| markVariableAsUsed(context, node.superClass.name); | ||
| if (!node.superTypeParameters || !node.superTypeParameters.params) { | ||
| return; | ||
| } | ||
| node.superTypeParameters.params.forEach(markTypeAnnotationAsUsed); | ||
| } | ||
| /** | ||
| * Checks the given interface and marks it as used. | ||
| * Generic arguments are also included in the check. | ||
| * This is used when interfaces are extending other interfaces. | ||
| * @param {ASTNode} node the relevant AST node. | ||
| * @returns {void} | ||
| * @private | ||
| */ | ||
| function markExtendedInterfaceAsUsed(node) { | ||
| if (!node || !node.id || !node.id.name) { | ||
| return; | ||
| } | ||
| markVariableAsUsed(context, node.id.name); | ||
| if (!node.typeParameters || !node.typeParameters.params) { | ||
| return; | ||
| } | ||
| node.typeParameters.params.forEach(markTypeAnnotationAsUsed); | ||
| } | ||
| /** | ||
| * Checks the given function return type and marks it as used. | ||
| * @param {ASTNode} node the relevant AST node. | ||
| * @returns {void} | ||
| * @private | ||
| */ | ||
| function markFunctionReturnTypeAsUsed(node) { | ||
| if (node.returnType) { | ||
| markTypeAnnotationAsUsed(node.returnType); | ||
| } | ||
| } | ||
| /** | ||
| * Checks the given class and marks super classes, interfaces and decoratores as used. | ||
| * @param {ASTNode} node the relevant AST node. | ||
| * @returns {void} | ||
| * @private | ||
| */ | ||
| function markClassOptionsAsUsed(node) { | ||
| markSuperClassAsUsed(node); | ||
| if (node.implements) { | ||
| node.implements.forEach(markImplementedInterfaceAsUsed); | ||
| } | ||
| if (node.decorators) { | ||
| node.decorators.forEach(markDecoratorAsUsed); | ||
| } | ||
| } | ||
| //---------------------------------------------------------------------- | ||
@@ -135,39 +233,32 @@ // Public | ||
| return { | ||
| ClassProperty: markDecoratorsAsUsed, | ||
| ClassDeclaration(node) { | ||
| markDecoratorsAsUsed(node); | ||
| markImplementedInterfacesAsUsed(node); | ||
| Identifier(node) { | ||
| if (node.typeAnnotation) { | ||
| markTypeAnnotationAsUsed(node.typeAnnotation); | ||
| } | ||
| if (node.decorators) { | ||
| node.decorators.forEach(markDecoratorAsUsed); | ||
| } | ||
| }, | ||
| MethodDefinition(node) { | ||
| /** | ||
| * Decorators are only supported on class methods, so exit early | ||
| * if the parent is not a ClassBody | ||
| */ | ||
| const anc = context.getAncestors(); | ||
| const tAnc = anc.length; | ||
| if ( | ||
| !tAnc || | ||
| !anc[tAnc - 1] || | ||
| anc[tAnc - 1].type !== "ClassBody" | ||
| ) { | ||
| return; | ||
| TypeAnnotation(node) { | ||
| if (node.typeAnnotation) { | ||
| markTypeAnnotationAsUsed(node.typeAnnotation); | ||
| } | ||
| }, | ||
| /** | ||
| * Mark any of the method's own decorators as used | ||
| */ | ||
| markDecoratorsAsUsed(node); | ||
| FunctionDeclaration: markFunctionReturnTypeAsUsed, | ||
| FunctionExpression: markFunctionReturnTypeAsUsed, | ||
| ArrowFunctionExpression: markFunctionReturnTypeAsUsed, | ||
| /** | ||
| * Mark any parameter decorators as used | ||
| */ | ||
| if ( | ||
| !node.value || | ||
| !node.value.params || | ||
| !node.value.params.length | ||
| ) { | ||
| return; | ||
| Decorator: markDecoratorAsUsed, | ||
| TSInterfaceHeritage: markExtendedInterfaceAsUsed, | ||
| ClassDeclaration: markClassOptionsAsUsed, | ||
| ClassExpression: markClassOptionsAsUsed, | ||
| MethodDefinition(node) { | ||
| if (node.decorators) { | ||
| node.decorators.forEach(markDecoratorAsUsed); | ||
| } | ||
| node.value.params.forEach(markDecoratorsAsUsed); | ||
| } | ||
@@ -174,0 +265,0 @@ }; |
@@ -36,3 +36,3 @@ /** | ||
| function isTypeScriptModuleDeclaration(node) { | ||
| return node.name && node.name.type === "Literal"; | ||
| return node.id && node.id.type === "Literal"; | ||
| } | ||
@@ -39,0 +39,0 @@ |
+3
-3
| { | ||
| "name": "eslint-plugin-typescript", | ||
| "version": "0.6.0", | ||
| "version": "0.7.0", | ||
| "description": "TypeScript plugin for ESLint", | ||
@@ -34,7 +34,7 @@ "keywords": [ | ||
| "typescript": "~2.4.2", | ||
| "typescript-eslint-parser": "^6.0.1" | ||
| "typescript-eslint-parser": "^7.0.0" | ||
| }, | ||
| "lint-staged": { | ||
| "*.js": [ | ||
| "prettier --write", | ||
| "prettier --write --tab-width 4", | ||
| "git add" | ||
@@ -41,0 +41,0 @@ ] |
@@ -24,2 +24,23 @@ /** | ||
| code: ` | ||
| function error(a: string); | ||
| function error(b: number); | ||
| function error(ab: string|number){ } | ||
| export { error }; | ||
| `, | ||
| parserOptions: { sourceType: "module" }, | ||
| parser: "typescript-eslint-parser" | ||
| }, | ||
| { | ||
| code: ` | ||
| import { connect } from 'react-redux'; | ||
| export interface ErrorMessageModel { message: string; } | ||
| function mapStateToProps() { } | ||
| function mapDispatchToProps() { } | ||
| export default connect(mapStateToProps, mapDispatchToProps)(ErrorMessage); | ||
| `, | ||
| parserOptions: { sourceType: "module" }, | ||
| parser: "typescript-eslint-parser" | ||
| }, | ||
| { | ||
| code: ` | ||
| export const foo = "a", bar = "b"; | ||
@@ -26,0 +47,0 @@ export interface Foo {} |
@@ -178,2 +178,237 @@ /** | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "interface Base {}", | ||
| "const a: Base = {}", | ||
| "console.log(a);" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Nullable<string> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'other'", | ||
| "const a: Nullable<SomeOther> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Nullable | undefined = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Nullable & undefined = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'other'", | ||
| "const a: Nullable<SomeOther[]> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'other'", | ||
| "const a: Nullable<Array<SomeOther>> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Array<Nullable> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Array<Nullable[]> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "const a: Array<Array<Nullable>> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'other'", | ||
| "const a: Array<Nullable<SomeOther>> = 'hello'", | ||
| "console.log(a)" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Component } from 'react'", | ||
| "class Foo implements Component<Nullable>{}", | ||
| "new Foo()" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Component } from 'react'", | ||
| "class Foo extends Component<Nullable, {}>{}", | ||
| "new Foo()" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Component } from 'react'", | ||
| "class Foo extends Component<Nullable<SomeOther>, {}>{}", | ||
| "new Foo()" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do = (a: Nullable<Another>) => { console.log(a); }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do(a: Nullable<Another>) { console.log(a); }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do(): Nullable<Another> { return null; }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "interface A {", | ||
| " do(a: Nullable<Another>);", | ||
| "}" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "interface A {", | ||
| " other: Nullable<Another>;", | ||
| "}" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "function foo(a: Nullable) { console.log(a); }", | ||
| "foo()" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "function foo(): Nullable { return null; }", | ||
| "foo()" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Another } from 'some'", | ||
| "class A extends Nullable<SomeOther> {", | ||
| " other: Nullable<Another>;", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Another } from 'some'", | ||
| "class A extends Nullable<SomeOther> {", | ||
| " do(a: Nullable<Another>){ console.log(a); }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Another } from 'some'", | ||
| "interface A extends Nullable<SomeOther> {", | ||
| " other: Nullable<Another>;", | ||
| "}" | ||
| ].join("\n"), | ||
| parser | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Another } from 'some'", | ||
| "interface A extends Nullable<SomeOther> {", | ||
| " do(a: Nullable<Another>);", | ||
| "}" | ||
| ].join("\n"), | ||
| parser | ||
| } | ||
@@ -197,4 +432,173 @@ ], | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable';", | ||
| "const a: string = 'hello';", | ||
| "console.log(a);" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Nullable' is defined but never used.", | ||
| line: 1, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable';", | ||
| "import { SomeOther } from 'other';", | ||
| "const a: Nullable<string> = 'hello';", | ||
| "console.log(a);" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'SomeOther' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do = (a: Nullable) => { console.log(a); }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Another' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do(a: Nullable) { console.log(a); }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Another' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "class A {", | ||
| " do(): Nullable { return null; }", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Another' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "interface A {", | ||
| " do(a: Nullable);", | ||
| "}" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Another' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { Another } from 'some'", | ||
| "interface A {", | ||
| " other: Nullable;", | ||
| "}" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Another' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "function foo(a: string) { console.log(a); }", | ||
| "foo()" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Nullable' is defined but never used.", | ||
| line: 1, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "function foo(): string | null { return null; }", | ||
| "foo()" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'Nullable' is defined but never used.", | ||
| line: 1, | ||
| column: 10 | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| code: [ | ||
| "import { Nullable } from 'nullable'", | ||
| "import { SomeOther } from 'some'", | ||
| "import { Another } from 'some'", | ||
| "class A extends Nullable {", | ||
| " other: Nullable<Another>;", | ||
| "}", | ||
| "new A();" | ||
| ].join("\n"), | ||
| parser, | ||
| errors: [ | ||
| { | ||
| message: "'SomeOther' is defined but never used.", | ||
| line: 2, | ||
| column: 10 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }); |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
595560
2.76%18669
2.77%