├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── helpers │ ├── attributeEventsRenamed.ts │ ├── attributeValuesRenamed.ts │ ├── attributesRenamed.ts │ ├── directiveToElement.ts │ ├── elementRename.ts │ ├── ionLabelRequired.ts │ ├── parametersRenamed.ts │ └── utils.ts ├── index.ts ├── ionActionSheetMethodCreateParametersRenamedRule.ts ├── ionAlertMethodCreateParametersRenamedRule.ts ├── ionBackButtonNotAddedByDefaultRule.ts ├── ionButtonAttributesRenamedRule.ts ├── ionButtonIsNowAnElementRule.ts ├── ionButtonsAttributesRenamedRule.ts ├── ionColAttributesRenamedRule.ts ├── ionDatetimeCapitalizationChangedRule.ts ├── ionFabAttributesRenamedRule.ts ├── ionFabButtonIsNowAnElementRule.ts ├── ionFabFixedContentRule.ts ├── ionIconAttributeIsActiveRemovedRule.ts ├── ionItemAttributesRenamedRule.ts ├── ionItemDividerIonLabelRequiredRule.ts ├── ionItemIonLabelRequiredRule.ts ├── ionItemIsNowAnElementRule.ts ├── ionItemOptionIsNowAnElementRule.ts ├── ionItemOptionMarkupHasChangedRule.ts ├── ionItemOptionMethodGetSlidingPercentRenamedRule.ts ├── ionItemOptionsAttributeValuesRenamedRule.ts ├── ionLabelAttributesRenamedRule.ts ├── ionListHeaderIonLabelRequiredRule.ts ├── ionLoadingMethodCreateParametersRenamedRule.ts ├── ionMenuEventsRenamedRule.ts ├── ionMenuToggleIsNowAnElementRule.ts ├── ionNavbarIsNowIonToolbarRule.ts ├── ionOptionIsNowIonSelectOptionRule.ts ├── ionOverlayMethodCreateShouldUseAwaitRule.ts ├── ionOverlayMethodPresentShouldUseAwaitRule.ts ├── ionRadioAttributesRenamedRule.ts ├── ionRadioGroupIsNowAnElementRule.ts ├── ionRadioSlotRequiredRule.ts ├── ionRangeAttributesRenamedRule.ts ├── ionSegmentButtonIonLabelRequiredRule.ts ├── ionSpinnerAttributeValuesRenamedRule.ts ├── ionTabsRefactoredRule.ts └── ionTextIsNowAnElementRule.ts ├── test ├── ionActionSheetMethodCreateParametersRenamed.spec.ts ├── ionAlertMethodCreateParametersRenamed.spec.ts ├── ionBackButtonNotAddedByDefault.spec.ts ├── ionButtonAttributesRenamed.spec.ts ├── ionButtonIsNowAnElement.spec.ts ├── ionButtonsAttributesRenamed.spec.ts ├── ionColAttributesRenamed.spec.ts ├── ionDatetimeCapitalizationChanged.spec.ts ├── ionFabAttributesRenamed.spec.ts ├── ionFabButtonIsNowAnElement.spec.ts ├── ionFabFixedContent.spec.ts ├── ionIconAttributeIsActiveRemoved.spec.ts ├── ionItemAttributesRenamed.spec.ts ├── ionItemDividerIonLabelRequired.spec.ts ├── ionItemIonLabelRequired.spec.ts ├── ionItemIsNowAnElement.spec.ts ├── ionItemOptionIsNowAnElement.spec.ts ├── ionItemOptionMarkupHasChanged.spec.ts ├── ionItemOptionMethodGetSlidingPercentRenamed.spec.ts ├── ionItemOptionsAttributeValuesRenamed.spec.ts ├── ionLabelAttributesRenamed.spec.ts ├── ionListHeaderIonLabelRequired.spec.ts ├── ionLoadingMethodCreateParametersRenamed.spec.ts ├── ionMenuEventsRenamed.spec.ts ├── ionMenuToggleIsNowAnElement.spec.ts ├── ionNavbarIsNowIonToolbarRule.spec.ts ├── ionOptionIsNowIonSelectOption.spec.ts ├── ionOverlayMethodCreateShouldUseAwaitRule.spec.ts ├── ionOverlayMethodPresentShouldUseAwaitRule.spec.ts ├── ionRadioAttributesRenamed.spec.ts ├── ionRadioGroupIsNowAnElement.spec.ts ├── ionRadioSlotRequired.spec.ts ├── ionRangeAttributesRenamed.spec.ts ├── ionSegmentButtonIonLabelRequired.spec.ts ├── ionSpinnerAttributeValuesRenamed.spec.ts ├── ionTabsRefactored.spec.ts ├── ionTextIsNowAnElement.spec.ts ├── testHelper.ts └── utils.ts ├── tsconfig.json ├── tsconfig.test.json └── tslint.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | aliases: 4 | - &restore-cache-deps 5 | key: dependency-cache-{{ checksum "package.json" }} 6 | 7 | - &save-cache-deps 8 | key: dependency-cache-{{ checksum "package.json" }} 9 | paths: 10 | - node_modules 11 | 12 | defaults: &defaults 13 | working_directory: /tmp/workspace 14 | docker: 15 | - image: circleci/node:8 16 | 17 | jobs: 18 | build: 19 | <<: *defaults 20 | steps: 21 | - checkout 22 | - restore_cache: *restore-cache-deps 23 | - run: npm install 24 | - save_cache: *save-cache-deps 25 | - run: npm run lint 26 | - run: npm run format:check 27 | - run: npm run build 28 | - persist_to_workspace: 29 | root: /tmp/workspace 30 | paths: 31 | - "*" 32 | test: 33 | <<: *defaults 34 | steps: 35 | - checkout 36 | - attach_workspace: 37 | at: /tmp/workspace 38 | - run: mkdir junit 39 | - run: 40 | command: npm run test:ci 41 | environment: 42 | MOCHA_FILE: junit/test-results.xml 43 | when: always 44 | - run: npm run test:coverage 45 | - store_test_results: 46 | path: junit 47 | - store_artifacts: 48 | path: junit 49 | path: coverage 50 | deploy: 51 | <<: *defaults 52 | environment: 53 | GIT_AUTHOR_NAME: Ionitron 54 | GIT_AUTHOR_EMAIL: hi@ionicframework.com 55 | GIT_COMMITTER_NAME: Ionitron 56 | GIT_COMMITTER_EMAIL: hi@ionicframework.com 57 | steps: 58 | - add_ssh_keys: 59 | fingerprints: 60 | - "fc:0c:f5:89:bf:37:42:27:a5:c5:f0:96:0d:93:9c:3e" # ionitron user key 61 | - checkout 62 | - attach_workspace: 63 | at: /tmp/workspace 64 | - run: npx semantic-release 65 | 66 | workflows: 67 | version: 2 68 | build-deploy: 69 | jobs: 70 | - build 71 | - test: 72 | requires: 73 | - build 74 | - deploy: 75 | requires: 76 | - test 77 | filters: 78 | branches: 79 | only: master 80 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | .coveralls.yml 20 | 21 | test-results.xml 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (http://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # Typescript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | src/**/*.js 64 | test/**/*.js 65 | rules 66 | package-lock.json 67 | .vscode 68 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 140, 3 | "singleQuote": true 4 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Clone the repo 4 | 1. `npm install` 5 | 1. `npm run build` to build source code 6 | 1. `npm run test` to run tests 7 | 8 | ### Workflow 9 | 10 | This repo uses [semantic-release](https://github.com/semantic-release/semantic-release), so it's important to follow a strict workflow to ensure properly automated releases. 11 | 12 | * Work off of `develop` branch (create new branch or fork) 13 | * Make changes with relevant test changes 14 | * Use `npm run cz` (or `git cz` if [commitizen](https://github.com/commitizen/cz-cli) is installed globally) to make commits 15 | * Create a pull request 16 | * Pull requests will be approved and squashed into the `develop` branch 17 | * Try to make pull requests with a single objective (don't have multiple features in one PR, don't mix fixes and features in one PR, etc.) 18 | 19 | ### Developing Rules 20 | 21 | Rules generally comprise two parts: a `Rule` class and a `RuleWalker` class. Rules which operate on TypeScript code can use extend `RuleWalker` directly from `tslint`, but rules which operate on markup or styles must use the `NgWalker` from [codelyzer](https://github.com/mgechev/codelyzer). 22 | 23 | #### Resources 24 | 25 | * [Developing TSLint rules](https://palantir.github.io/tslint/develop/custom-rules/) 26 | 27 | ### Linking the Rules into a Project 28 | 29 | Do not use `npm link`! 30 | 31 | 1. `npm run tsc:watch` to watch for source changes 32 | 1. Copy the rules into your project's `node_modules`: 33 | 34 | ``` 35 | rsync --exclude node_modules --exclude .git -rv /path/to/v4-migration-tslint node_modules/@ionic 36 | ``` 37 | 38 | 1. Follow usage instructions in [`README.md`](https://github.com/ionic-team/v4-migration-tslint/blob/develop/README.md#how-to-use) 39 | 40 | ### Publishing 41 | 42 | Releases are automated in CI using [semantic-release](https://github.com/semantic-release/semantic-release) when the `master` branch is pushed to Github. Rebase `develop` with `master`. Commits in `develop` should be appropriately formatted from the PR workflow (see [Workflow](#workflow)). 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Drifty Co 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ionic/v4-migration-tslint", 3 | "version": "1.7.1", 4 | "description": "TSLint rules for Ionic Angular v4 Migration", 5 | "main": "rules/index.js", 6 | "scripts": { 7 | "clean": "rimraf dist rules 'src/**/*.js' 'test/**/*.js' coverage test-results.xml .nyc_output", 8 | "format:base": "prettier --config ./.prettierrc \"*.{json}\" \"src/**/*.{css,scss,ts}\" \"test/**/*.{css,scss,ts}\"", 9 | "format:check": "npm run format:base -- --list-different", 10 | "format:fix": "npm run format:base -- --write", 11 | "lint": "tslint -c tslint.json \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "lint:fix": "npm run lint -- --fix", 13 | "build": "npm run clean && npm run tsc", 14 | "src:tsc": "tsc", 15 | "src:tsc:watch": "npm run src:tsc -- -w", 16 | "test": "mocha --recursive", 17 | "test:tsc": "tsc -p tsconfig.test.json", 18 | "test:tsc:watch": "npm run test:tsc -- -w", 19 | "test:ci": "npm run test -- --reporter mocha-junit-reporter", 20 | "test:watch": "mocha --watch --recursive", 21 | "test:coverage": "npm run clean && npm run build && nyc --reporter=lcov --reporter=text-lcov npm test && nyc report --reporter=text-lcov | coveralls", 22 | "tsc": "npm run src:tsc && npm run test:tsc", 23 | "tsc:watch": "concurrently --names 'src,test' 'npm run src:tsc:watch -- --preserveWatchOutput' 'npm run test:tsc:watch -- --preserveWatchOutput'", 24 | "cz": "git-cz" 25 | }, 26 | "files": [ 27 | "rules" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/ionic-team/v4-migration-tslint.git" 32 | }, 33 | "author": "", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/ionic-team/v4-migration-tslint/issues" 37 | }, 38 | "homepage": "https://github.com/ionic-team/v4-migration-tslint#readme", 39 | "dependencies": { 40 | "codelyzer": "^4.4.4", 41 | "tslint": "^5.0.0" 42 | }, 43 | "devDependencies": { 44 | "@angular/compiler": "^7.1.3", 45 | "@angular/core": "^7.1.3", 46 | "@semantic-release/changelog": "^3.0.0", 47 | "@semantic-release/git": "^7.0.4", 48 | "@types/chai": "^4.1.3", 49 | "@types/mocha": "^5.2.0", 50 | "@types/node": "^12.0.0", 51 | "@types/sprintf-js": "^1.1.0", 52 | "chai": "^4.1.2", 53 | "commitizen": "^3.0.2", 54 | "concurrently": "^4.0.1", 55 | "coveralls": "^3.0.1", 56 | "cz-conventional-changelog": "^2.1.0", 57 | "husky": "^3.0.0", 58 | "mocha": "^5.1.1", 59 | "mocha-junit-reporter": "^1.17.0", 60 | "nyc": "^14.0.0", 61 | "path": "^0.12.7", 62 | "prettier": "^1.12.1", 63 | "rimraf": "^2.6.2", 64 | "rxjs": "^6.2.0", 65 | "semantic-release": "^15.1.8", 66 | "typescript": "~2.9.2" 67 | }, 68 | "peerDependencies": { 69 | "@angular/compiler": "^7.1.3", 70 | "@angular/core": "^7.1.3" 71 | }, 72 | "release": { 73 | "verifyConditions": [ 74 | "@semantic-release/changelog", 75 | "@semantic-release/npm", 76 | "@semantic-release/github", 77 | "@semantic-release/git" 78 | ], 79 | "prepare": [ 80 | "@semantic-release/changelog", 81 | "@semantic-release/npm", 82 | "@semantic-release/git" 83 | ], 84 | "publish": [ 85 | "@semantic-release/github", 86 | "@semantic-release/npm" 87 | ], 88 | "success": [ 89 | "@semantic-release/github" 90 | ], 91 | "fail": [ 92 | "@semantic-release/github" 93 | ] 94 | }, 95 | "husky": { 96 | "hooks": { 97 | "pre-commit": "npm run lint && npm run format:check" 98 | } 99 | }, 100 | "nyc": { 101 | "report-dir": "./coverage", 102 | "exclude": [ 103 | "test/*.js", 104 | "src/**/*.js" 105 | ] 106 | }, 107 | "config": { 108 | "commitizen": { 109 | "path": "./node_modules/cz-conventional-changelog" 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/helpers/attributeEventsRenamed.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import * as Lint from 'tslint'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | 5 | function generateErrorMessage(elementName: string, attrName: string, replacement: string) { 6 | return `The ${attrName} event of ${elementName} has been renamed. Use ${replacement} instead.`; 7 | } 8 | 9 | export function createAttributesRenamedTemplateVisitorClass(elementNames: string[] | undefined, replacementMap: Map) { 10 | return class extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | if (!elementNames || elementNames.includes(element.name)) { 13 | this.checkAttributesForReplacements(element); 14 | } 15 | 16 | super.visitElement(element, context); 17 | } 18 | 19 | private checkAttributesForReplacements(element: ast.ElementAst) { 20 | for (const output of element.outputs) { 21 | const replacement = replacementMap.get(output.name); 22 | 23 | if (replacement) { 24 | const start = output.sourceSpan.start.offset + 1; 25 | const length = output.name.length; 26 | const position = this.getSourcePosition(start); 27 | 28 | this.addFailureAt(start, length, generateErrorMessage(element.name, output.name, replacement), [ 29 | Lint.Replacement.replaceFromTo(position, position + length, replacement) 30 | ]); 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers/attributeValuesRenamed.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import * as Lint from 'tslint'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | 5 | function generateErrorMessage(elementName: string, attrName: string, attrValue: string, replacement: string) { 6 | return `The ${attrName}="${attrValue}" attribute/value of ${elementName} should be written as ${replacement}.`; 7 | } 8 | 9 | export function createAttributeValuesRenamedTemplateVisitorClass(elementNames: string[], replacementMap: Map>) { 10 | return class extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | for (const elementName of elementNames) { 13 | if (element.name === elementName) { 14 | for (const attr of element.attrs) { 15 | const attrValueMap = replacementMap.get(attr.name); 16 | 17 | if (attrValueMap) { 18 | const replacementValue = attrValueMap.get(attr.value); 19 | 20 | if (replacementValue) { 21 | const start = attr.sourceSpan.start.offset; 22 | const end = attr.sourceSpan.end.offset; 23 | const position = this.getSourcePosition(start); 24 | const replacement = `${attr.name}="${replacementValue}"`; 25 | 26 | this.addFailureAt(start, end - start, generateErrorMessage(elementName, attr.name, attr.value, replacement), [ 27 | Lint.Replacement.replaceFromTo(position, position + end - start, replacement) 28 | ]); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | 35 | super.visitElement(element, context); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/helpers/attributesRenamed.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import * as Lint from 'tslint'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | 5 | function generateErrorMessage(elementName: string, attrName: string, replacement: string) { 6 | return `The ${attrName} attribute of ${elementName} has been renamed. Use ${replacement} instead.`; 7 | } 8 | 9 | export function createAttributesRenamedTemplateVisitorClass(elementNames: string[] | undefined, replacementMap: Map) { 10 | return class extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | if (!elementNames || elementNames.includes(element.name)) { 13 | this.checkAttributesForReplacements(element); 14 | } 15 | 16 | super.visitElement(element, context); 17 | } 18 | 19 | private checkAttributesForReplacements(element: ast.ElementAst) { 20 | for (const attr of element.attrs) { 21 | const replacement = replacementMap.get(attr.name); 22 | 23 | if (replacement) { 24 | const start = attr.sourceSpan.start.offset; 25 | const length = attr.name.length; 26 | const position = this.getSourcePosition(start); 27 | 28 | this.addFailureAt(start, length, generateErrorMessage(element.name, attr.name, replacement), [ 29 | Lint.Replacement.replaceFromTo(position, position + length, replacement) 30 | ]); 31 | } 32 | } 33 | } 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /src/helpers/directiveToElement.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as ts from 'typescript'; 6 | 7 | export function generateDescription(directive: string, resultantElement: string) { 8 | return `${directive} is now an ${resultantElement} element instead of an Angular directive.`; 9 | } 10 | export type ReplacementLevel = 'parent' | 'same' | 'child'; 11 | 12 | export function createDirectiveToElementTemplateVisitorClass(directive: string, resultantElement: string, level: ReplacementLevel) { 13 | return class extends BasicTemplateAstVisitor { 14 | visitElement(element: ast.ElementAst, context: any): any { 15 | const foundAttr = element.attrs.find(attr => attr.name === directive); 16 | 17 | if (foundAttr) { 18 | const attributeStart = foundAttr.sourceSpan.start.offset; 19 | const attributeLength = directive.length; 20 | const attributePosition = this.getSourcePosition(attributeStart); 21 | 22 | const fixes = [Lint.Replacement.deleteFromTo(attributePosition - 1, attributePosition + attributeLength)]; 23 | 24 | switch (level) { 25 | case 'parent': 26 | const parentOpenTag = `<${resultantElement}>\n`; 27 | const parentCloseTag = `\n`; 28 | 29 | const angleBracketStartPosition = this.getSourcePosition(element.sourceSpan.start.offset); 30 | const closingAngleBracketEndPosition = this.getSourcePosition(element.endSourceSpan.end.offset); 31 | 32 | fixes.push( 33 | Lint.Replacement.replaceFromTo(angleBracketStartPosition, angleBracketStartPosition, parentOpenTag), 34 | Lint.Replacement.replaceFromTo(closingAngleBracketEndPosition, closingAngleBracketEndPosition, parentCloseTag) 35 | ); 36 | break; 37 | case 'same': 38 | const tagNameLength = element.name.length; 39 | const tagNameStartPosition = this.getSourcePosition(element.sourceSpan.start.offset) + 1; 40 | const closingTagNameStartPosition = this.getSourcePosition(element.endSourceSpan.start.offset) + 2; 41 | 42 | fixes.push( 43 | Lint.Replacement.replaceFromTo(tagNameStartPosition, tagNameStartPosition + tagNameLength, resultantElement), 44 | Lint.Replacement.replaceFromTo(closingTagNameStartPosition, closingTagNameStartPosition + tagNameLength, resultantElement) 45 | ); 46 | break; 47 | case 'child': 48 | const childOpenTag = `>\n<${resultantElement}>`; 49 | const childCloseTag = `\n<`; 50 | 51 | const angleBracketEndPosition = this.getSourcePosition(element.sourceSpan.end.offset); 52 | const closingAngleBracketStartPosition = this.getSourcePosition(element.endSourceSpan.start.offset); 53 | 54 | fixes.push( 55 | Lint.Replacement.replaceFromTo(angleBracketEndPosition - 1, angleBracketEndPosition, childOpenTag), 56 | Lint.Replacement.replaceFromTo(closingAngleBracketStartPosition, closingAngleBracketStartPosition + 1, childCloseTag) 57 | ); 58 | break; 59 | } 60 | 61 | this.addFailureAt(attributeStart, attributeLength, generateDescription(directive, resultantElement), fixes); 62 | } 63 | 64 | super.visitElement(element, context); 65 | } 66 | }; 67 | } 68 | 69 | export function createDirectiveToElementRuleClass( 70 | ruleName: string, 71 | directive: string, 72 | resultantElement = directive, 73 | level: ReplacementLevel = 'same' 74 | ) { 75 | return class extends Lint.Rules.AbstractRule { 76 | public static metadata: Lint.IRuleMetadata = { 77 | ruleName: ruleName, 78 | type: 'functionality', 79 | description: generateDescription(directive, resultantElement), 80 | options: null, 81 | optionsDescription: 'Not configurable.', 82 | typescriptOnly: false, 83 | hasFix: true 84 | }; 85 | 86 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 87 | return this.applyWithWalker( 88 | new NgWalker(sourceFile, this.getOptions(), { 89 | templateVisitorCtrl: createDirectiveToElementTemplateVisitorClass(directive, resultantElement, level) 90 | }) 91 | ); 92 | } 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/helpers/elementRename.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as ts from 'typescript'; 6 | 7 | function generateDescription(elementName: string, newElementName: string) { 8 | return `The ${elementName} component is now named ${newElementName}.`; 9 | } 10 | 11 | export function createElementRenameTemplateVisitorClass(elementName: string, newElementName: string) { 12 | return class extends BasicTemplateAstVisitor { 13 | visitElement(element: ast.ElementAst, context: any): any { 14 | if (element.name && element.name === elementName) { 15 | const startTagStart = element.sourceSpan.start.offset; 16 | const startTagLength = element.name.length; 17 | const startTagPosition = this.getSourcePosition(startTagStart) + 1; 18 | const endTagStart = element.endSourceSpan.start.offset; 19 | const endTagLength = element.name.length; 20 | const endTagPosition = this.getSourcePosition(endTagStart) + 2; 21 | 22 | this.addFailureAt(startTagStart + 1, startTagLength, generateDescription(element.name, newElementName), [ 23 | Lint.Replacement.replaceFromTo(startTagPosition, startTagPosition + startTagLength, newElementName), 24 | Lint.Replacement.replaceFromTo(endTagPosition, endTagPosition + endTagLength, newElementName) 25 | ]); 26 | } 27 | 28 | super.visitElement(element, context); 29 | } 30 | }; 31 | } 32 | 33 | export function createElementRenameRuleClass(ruleName: string, elementName: string, newElementName: string) { 34 | return class extends Lint.Rules.AbstractRule { 35 | public static metadata: Lint.IRuleMetadata = { 36 | ruleName: ruleName, 37 | type: 'functionality', 38 | description: generateDescription(elementName, newElementName), 39 | options: null, 40 | optionsDescription: 'Not configurable.', 41 | typescriptOnly: false, 42 | hasFix: true 43 | }; 44 | 45 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 46 | return this.applyWithWalker( 47 | new NgWalker(sourceFile, this.getOptions(), { 48 | templateVisitorCtrl: createElementRenameTemplateVisitorClass(elementName, newElementName) 49 | }) 50 | ); 51 | } 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/helpers/ionLabelRequired.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 3 | 4 | import { isElementAst } from './utils'; 5 | 6 | export function createIonLabelRequiredTemplateVisitorClass(elementName: string) { 7 | return class extends BasicTemplateAstVisitor { 8 | visitElement(element: ast.ElementAst, context: any): any { 9 | if (element.name && element.name === elementName) { 10 | const ionLabelElement = element.children.find((e): e is ast.ElementAst => isElementAst(e) && e.name === 'ion-label'); 11 | 12 | if (!ionLabelElement) { 13 | const start = element.sourceSpan.start.offset; 14 | const length = element.name.length; 15 | const position = this.getSourcePosition(start) + length + 1; 16 | 17 | this.addFailureAt(start + 1, length, `The ${elementName} requires an ion-label component. It is no longer automatically added.`); 18 | } 19 | } 20 | 21 | super.visitElement(element, context); 22 | } 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/helpers/parametersRenamed.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as tsutils from 'tsutils'; 3 | import * as ts from 'typescript'; 4 | 5 | /** 6 | * Currently only supports a call exactly like this: 7 | * 8 | * ``` 9 | * this.ctrl.foo({ prop1: , prop2: }) 10 | * ``` 11 | * 12 | * `this.ctrl` must refer to a provider by name in the constructor of a class 13 | * using Angular dependency injection 14 | * 15 | * @param methodName would be `foo` in example above 16 | * @param providerName the provider class name 17 | * @param parameterMap a map of properties to rename in the object literal above 18 | */ 19 | export function createParametersRenamedClass(methodName: string, providerName: string, parameterMap: Map) { 20 | return class extends Lint.RuleWalker { 21 | visitCallExpression(node: ts.CallExpression) { 22 | if (node.arguments.length > 0) { 23 | const firstArgument = node.arguments[0]; 24 | 25 | if (isValidForRule(node, methodName, providerName) && tsutils.isObjectLiteralExpression(firstArgument)) { 26 | for (const prop of firstArgument.properties) { 27 | if (tsutils.isPropertyAssignment(prop) || tsutils.isShorthandPropertyAssignment(prop)) { 28 | const propName = tsutils.getPropertyName(prop.name); 29 | const replacementPropName = parameterMap.get(propName); 30 | 31 | if (replacementPropName) { 32 | const replacement = tsutils.isShorthandPropertyAssignment(prop) 33 | ? `${replacementPropName}: ${propName}` 34 | : replacementPropName; 35 | this.addFailureAtNode( 36 | prop.name, 37 | `Property ${propName} has been renamed to ${replacementPropName}.`, 38 | new Lint.Replacement(prop.name.getStart(), prop.name.getWidth(), replacement) 39 | ); 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | super.visitCallExpression(node); 47 | } 48 | }; 49 | } 50 | 51 | export function isValidForRule(node: ts.CallExpression, methodName: string, ...providerNames: string[]): boolean { 52 | const expression = node.expression; 53 | 54 | if ( 55 | tsutils.isPropertyAccessExpression(expression) && 56 | expression.name.text === methodName && 57 | tsutils.isPropertyAccessExpression(expression.expression) && 58 | expression.expression.expression.kind === ts.SyntaxKind.ThisKeyword 59 | ) { 60 | const classNode = findDeclarativeClass(node); 61 | const controllerName = expression.expression.name.text; 62 | 63 | if (classNode) { 64 | const constructorNode = classNode.members.find(n => tsutils.isConstructorDeclaration(n)); 65 | 66 | if (constructorNode && tsutils.isConstructorDeclaration(constructorNode)) { 67 | const controllerParameter = constructorNode.parameters.find( 68 | p => tsutils.isTypeReferenceNode(p.type) && tsutils.isIdentifier(p.name) && p.name.text === controllerName 69 | ); 70 | 71 | if ( 72 | controllerParameter && 73 | tsutils.isTypeReferenceNode(controllerParameter.type) && 74 | tsutils.isIdentifier(controllerParameter.type.typeName) && 75 | providerNames.indexOf(controllerParameter.type.typeName.text) > -1 76 | ) { 77 | return true; 78 | } 79 | } 80 | } 81 | } 82 | 83 | return false; 84 | } 85 | 86 | function findDeclarativeClass(node: ts.Node): ts.ClassDeclaration | undefined { 87 | if (!node) { 88 | return; 89 | } 90 | 91 | if (tsutils.isClassDeclaration(node)) { 92 | return node; 93 | } 94 | 95 | return findDeclarativeClass(node.parent); 96 | } 97 | -------------------------------------------------------------------------------- /src/helpers/utils.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | 3 | export function isElementAst(node: ast.TemplateAst): node is ast.ElementAst { 4 | const n = node as ast.ElementAst; 5 | return n && typeof n.children === 'object' && typeof n.name === 'string' && typeof n.attrs === 'object'; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // this file exists for tslint to resolve the rules directory 2 | export const rulesDirectory = '.'; 3 | -------------------------------------------------------------------------------- /src/ionActionSheetMethodCreateParametersRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as ts from 'typescript'; 3 | 4 | import { createParametersRenamedClass } from './helpers/parametersRenamed'; 5 | 6 | export const ruleName = 'ion-action-sheet-method-create-parameters-renamed'; 7 | 8 | const parameterMap = new Map([['title', 'header'], ['subTitle', 'subHeader']]); 9 | const Walker = createParametersRenamedClass('create', 'ActionSheetController', parameterMap); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'ActionSheetController now takes in different parameters to its create method.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: true, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ionAlertMethodCreateParametersRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as ts from 'typescript'; 3 | 4 | import { createParametersRenamedClass } from './helpers/parametersRenamed'; 5 | 6 | export const ruleName = 'ion-alert-method-create-parameters-renamed'; 7 | 8 | const parameterMap = new Map([['title', 'header'], ['subTitle', 'subHeader']]); 9 | const Walker = createParametersRenamedClass('create', 'AlertController', parameterMap); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'AlertController now takes in different parameters to its create method.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: true, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ionBackButtonNotAddedByDefaultRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as tsutils from 'tsutils'; 6 | import * as ts from 'typescript'; 7 | 8 | import { isElementAst } from './helpers/utils'; 9 | 10 | export const ruleName = 'ion-back-button-not-added-by-default'; 11 | 12 | class TemplateVisitor extends BasicTemplateAstVisitor { 13 | visitElement(element: ast.ElementAst, context: any): any { 14 | if (element.name && element.name === 'ion-toolbar') { 15 | let found = false; 16 | const ionButtonsElement = element.children.find((e): e is ast.ElementAst => isElementAst(e) && e.name === 'ion-buttons'); 17 | 18 | if (ionButtonsElement) { 19 | const ionBackButtonElement = ionButtonsElement.children.find(e => isElementAst(e) && e.name === 'ion-back-button'); 20 | 21 | if (ionBackButtonElement) { 22 | found = true; 23 | } 24 | } 25 | 26 | if (!found) { 27 | const start = element.sourceSpan.start.offset; 28 | const length = element.name.length; 29 | const position = this.getSourcePosition(start) + length + 1; 30 | 31 | this.addFailureAt(start + 1, length, 'The back button in an ion-toolbar is no longer automatically added.'); 32 | } 33 | } 34 | 35 | super.visitElement(element, context); 36 | } 37 | } 38 | 39 | export class Rule extends Lint.Rules.AbstractRule { 40 | public static metadata: Lint.IRuleMetadata = { 41 | ruleName: ruleName, 42 | type: 'functionality', 43 | description: 'The ion-back-button is not added by default to an ion-toolbar.', 44 | options: null, 45 | optionsDescription: 'Not configurable.', 46 | typescriptOnly: false, 47 | hasFix: true 48 | }; 49 | 50 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 51 | return this.applyWithWalker( 52 | new NgWalker(sourceFile, this.getOptions(), { 53 | templateVisitorCtrl: TemplateVisitor 54 | }) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ionButtonAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-button-attributes-renamed'; 8 | 9 | const replacementMap = new Map([ 10 | ['icon-left', 'slot="start"'], 11 | ['icon-start', 'slot="start"'], 12 | ['icon-right', 'slot="end"'], 13 | ['icon-end', 'slot="end"'], 14 | ['small', 'size="small"'], 15 | ['large', 'size="large"'], 16 | ['clear', 'fill="clear"'], 17 | ['outline', 'fill="outline"'], 18 | ['solid', 'fill="solid"'], 19 | ['full', 'expand="full"'], 20 | ['block', 'expand="block"'], 21 | ['round', 'shape="round"'] 22 | ]); 23 | 24 | const IonButtonAttributesAreRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-button'], replacementMap); 25 | 26 | export class Rule extends Lint.Rules.AbstractRule { 27 | public static metadata: Lint.IRuleMetadata = { 28 | ruleName: ruleName, 29 | type: 'functionality', 30 | description: 'Attributes of ion-button have been renamed.', 31 | options: null, 32 | optionsDescription: 'Not configurable.', 33 | typescriptOnly: false, 34 | hasFix: true 35 | }; 36 | 37 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 38 | return this.applyWithWalker( 39 | new NgWalker(sourceFile, this.getOptions(), { 40 | templateVisitorCtrl: IonButtonAttributesAreRenamedTemplateVisitor 41 | }) 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ionButtonIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'ion-button'; 4 | 5 | export const ruleName = 'ion-button-is-now-an-element'; 6 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive); 7 | -------------------------------------------------------------------------------- /src/ionButtonsAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-buttons-attributes-renamed'; 8 | 9 | const replacementMap = new Map([ 10 | ['start', 'slot="secondary"'], 11 | ['end', 'slot="primary"'], 12 | ['left', 'slot="start"'], 13 | ['right', 'slot="end"'] 14 | ]); 15 | 16 | const TemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-buttons'], replacementMap); 17 | 18 | export class Rule extends Lint.Rules.AbstractRule { 19 | public static metadata: Lint.IRuleMetadata = { 20 | ruleName: ruleName, 21 | type: 'functionality', 22 | description: 'Attributes of ion-buttons have been renamed.', 23 | options: null, 24 | optionsDescription: 'Not configurable.', 25 | typescriptOnly: false, 26 | hasFix: true 27 | }; 28 | 29 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 30 | return this.applyWithWalker( 31 | new NgWalker(sourceFile, this.getOptions(), { 32 | templateVisitorCtrl: TemplateVisitor 33 | }) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ionColAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-col-attributes-renamed'; 8 | 9 | const formatOldAttr = (prefix: string, breakpoint: string | undefined, value: string) => 10 | `${prefix}${typeof breakpoint === 'undefined' ? '' : `-${breakpoint}`}-${value}`; 11 | const formatNewAttr = (prefix: string, breakpoint: string | undefined, value: string) => 12 | `${prefix}${typeof breakpoint === 'undefined' ? '' : `-${breakpoint}`}="${value}"`; 13 | 14 | const attrPrefixMap = new Map([['col', 'size'], ['offset', 'offset'], ['push', 'push'], ['pull', 'pull']]); 15 | const breakpoints = [undefined, 'xs', 'sm', 'md', 'lg', 'xl']; 16 | const values = ['auto', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; 17 | 18 | const replacementPairs: [string, string][] = [].concat( 19 | ...[...attrPrefixMap.entries()].map(([oldPrefix, newPrefix]) => 20 | [].concat(...breakpoints.map(b => values.map(v => [formatOldAttr(oldPrefix, b, v), formatNewAttr(newPrefix, b, v)]))) 21 | ) 22 | ); 23 | 24 | const replacementMap = new Map(replacementPairs); 25 | 26 | const IonGridAttributesRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-col'], replacementMap); 27 | 28 | export class Rule extends Lint.Rules.AbstractRule { 29 | public static metadata: Lint.IRuleMetadata = { 30 | ruleName: ruleName, 31 | type: 'functionality', 32 | description: 'Attributes of ion-col have been renamed.', 33 | options: null, 34 | optionsDescription: 'Not configurable.', 35 | typescriptOnly: false, 36 | hasFix: true 37 | }; 38 | 39 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 40 | return this.applyWithWalker( 41 | new NgWalker(sourceFile, this.getOptions(), { 42 | templateVisitorCtrl: IonGridAttributesRenamedTemplateVisitor 43 | }) 44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/ionDatetimeCapitalizationChangedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as ts from 'typescript'; 3 | 4 | export const ruleName = 'ion-datetime-capitalization-changed'; 5 | export class Rule extends Lint.Rules.AbstractRule { 6 | public static metadata: Lint.IRuleMetadata = { 7 | ruleName, 8 | type: 'functionality', 9 | description: 'Updates the Datetime import', 10 | rationale: 'Datetime exported symbol has changed', 11 | options: null, 12 | optionsDescription: 'Not configurable.', 13 | typescriptOnly: true 14 | }; 15 | 16 | static RuleFailure = 'outdated import path'; 17 | 18 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 19 | return this.applyWithWalker(new UpdateImportsWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | class UpdateImportsWalker extends Lint.RuleWalker { 24 | visitImportDeclaration(node: ts.ImportDeclaration): void { 25 | if (ts.isStringLiteral(node.moduleSpecifier) && node.importClause) { 26 | const specifier = node.moduleSpecifier; 27 | const path = (specifier as ts.StringLiteral).text; 28 | const start = specifier.getStart() + 1; 29 | const end = specifier.text.length; 30 | const replacementStart = start; 31 | const replacementEnd = specifier.text.length; 32 | let replacement = null; 33 | 34 | // Try to find updated symbol names. 35 | ImportReplacements.forEach(r => (r.path === path ? this._migrateExportedSymbols(r, node) : void 0)); 36 | 37 | // Try to migrate entire import path updates. 38 | if (ImportMap.has(path)) { 39 | replacement = ImportMap.get(path); 40 | } 41 | if (replacement !== null) { 42 | return this.addFailureAt(start, end, Rule.RuleFailure, this.createReplacement(replacementStart, replacementEnd, replacement)); 43 | } 44 | } 45 | } 46 | 47 | private _migrateExportedSymbols(re: ImportReplacement, node: ts.ImportDeclaration) { 48 | const importClause = node.importClause as ts.ImportClause; 49 | const bindings = importClause.namedBindings as ts.NamedImports | null; 50 | if (!bindings || bindings.kind !== ts.SyntaxKind.NamedImports) { 51 | return; 52 | } 53 | 54 | bindings.elements.forEach((e: ts.ImportSpecifier | null) => { 55 | if (!e || e.kind !== ts.SyntaxKind.ImportSpecifier) { 56 | return; 57 | } 58 | 59 | let toReplace = e.name; 60 | // We don't want to introduce type errors so we alias the old new symbol. 61 | let replacement = `${re.newSymbol} as ${re.symbol}`; 62 | if (e.propertyName) { 63 | toReplace = e.propertyName; 64 | replacement = re.newSymbol; 65 | } 66 | 67 | if (toReplace.getText() !== re.symbol) { 68 | return; 69 | } 70 | 71 | return this.addFailureAt( 72 | toReplace.getStart(), 73 | toReplace.getWidth(), 74 | 'imported symbol no longer exists', 75 | this.createReplacement(toReplace.getStart(), toReplace.getWidth(), replacement) 76 | ); 77 | }); 78 | } 79 | } 80 | 81 | const ImportMap = new Map([['ionic-angular', '@ionic/angular']]); 82 | 83 | interface ImportReplacement { 84 | path: string; 85 | symbol: string; 86 | newPath: string; 87 | newSymbol: string; 88 | } 89 | 90 | const ImportReplacements = [ 91 | { 92 | path: 'ionic-angular', 93 | symbol: 'DateTime', 94 | newPath: '@ionic/angular', 95 | newSymbol: 'Datetime' 96 | }, 97 | { 98 | path: '@ionic/angular', 99 | symbol: 'DateTime', 100 | newPath: '@ionic/angular', 101 | newSymbol: 'Datetime' 102 | } 103 | ]; 104 | -------------------------------------------------------------------------------- /src/ionFabAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-fab-attributes-renamed'; 8 | 9 | const replacementMap = new Map([ 10 | ['center', 'horizontal="center"'], 11 | ['start', 'horizontal="start"'], 12 | ['end', 'horizontal="end"'], 13 | ['top', 'vertical="top"'], 14 | ['bottom', 'vertical="bottom"'], 15 | ['middle', 'vertical="center"'] 16 | ]); 17 | 18 | const IonFabAttributesRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-fab'], replacementMap); 19 | 20 | export class Rule extends Lint.Rules.AbstractRule { 21 | public static metadata: Lint.IRuleMetadata = { 22 | ruleName: ruleName, 23 | type: 'functionality', 24 | description: 'Attributes of ion-fab have been renamed.', 25 | options: null, 26 | optionsDescription: 'Not configurable.', 27 | typescriptOnly: false, 28 | hasFix: true 29 | }; 30 | 31 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 32 | return this.applyWithWalker( 33 | new NgWalker(sourceFile, this.getOptions(), { 34 | templateVisitorCtrl: IonFabAttributesRenamedTemplateVisitor 35 | }) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ionFabButtonIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'ion-fab'; 4 | const resultantElement = 'ion-fab-button'; 5 | 6 | export const ruleName = 'ion-fab-button-is-now-an-element'; 7 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive, resultantElement); 8 | -------------------------------------------------------------------------------- /src/ionFabFixedContentRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as tsutils from 'tsutils'; 6 | import * as ts from 'typescript'; 7 | 8 | export const ruleName = 'ion-fab-fixed-content'; 9 | 10 | class TemplateVisitor extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | if (element.name && element.name === 'ion-fab') { 13 | const attributeFound = element.attrs.find(attr => attr.name === 'slot'); 14 | 15 | if (!attributeFound || attributeFound.value !== 'fixed') { 16 | const start = element.sourceSpan.start.offset; 17 | const length = element.name.length; 18 | const position = this.getSourcePosition(start) + length + 1; 19 | 20 | this.addFailureAt(start + 1, length, 'The ion-fab container is no longer fixed by default. Use slot="fixed".', [ 21 | Lint.Replacement.replaceFromTo(position, position, ' slot="fixed"') 22 | ]); 23 | } 24 | } 25 | 26 | super.visitElement(element, context); 27 | } 28 | } 29 | 30 | export class Rule extends Lint.Rules.AbstractRule { 31 | public static metadata: Lint.IRuleMetadata = { 32 | ruleName: ruleName, 33 | type: 'functionality', 34 | description: 'The ion-fab container is no longer fixed by default. Use slot="fixed".', 35 | options: null, 36 | optionsDescription: 'Not configurable.', 37 | typescriptOnly: false, 38 | hasFix: true 39 | }; 40 | 41 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 42 | return this.applyWithWalker( 43 | new NgWalker(sourceFile, this.getOptions(), { 44 | templateVisitorCtrl: TemplateVisitor 45 | }) 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ionIconAttributeIsActiveRemovedRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as tsutils from 'tsutils'; 6 | import * as ts from 'typescript'; 7 | 8 | export const ruleName = 'ion-icon-attribute-is-active-removed'; 9 | 10 | class TemplateVisitor extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | if (element.name && element.name === 'ion-icon') { 13 | const attributeFound = element.attrs.find(attr => attr.name === 'isActive'); 14 | 15 | if (attributeFound) { 16 | const start = attributeFound.sourceSpan.start.offset; 17 | const end = attributeFound.sourceSpan.end.offset; 18 | 19 | this.addFailureAt(start, end - start, 'The isActive attribute of ion-icon has been removed.'); 20 | } 21 | } 22 | 23 | super.visitElement(element, context); 24 | } 25 | } 26 | 27 | export class Rule extends Lint.Rules.AbstractRule { 28 | public static metadata: Lint.IRuleMetadata = { 29 | ruleName: ruleName, 30 | type: 'functionality', 31 | description: 'The isActive attribute of ion-icon has been removed.', 32 | options: null, 33 | optionsDescription: 'Not configurable.', 34 | typescriptOnly: false, 35 | hasFix: true 36 | }; 37 | 38 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 39 | return this.applyWithWalker( 40 | new NgWalker(sourceFile, this.getOptions(), { 41 | templateVisitorCtrl: TemplateVisitor 42 | }) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ionItemAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-item-attributes-renamed'; 8 | 9 | const replacementMap = new Map([ 10 | ['item-start', 'slot="start"'], 11 | ['item-left', 'slot="start"'], 12 | ['item-end', 'slot="end"'], 13 | ['item-right', 'slot="end"'] 14 | ]); 15 | 16 | const IonItemAttributesRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-item'], replacementMap); 17 | 18 | export class Rule extends Lint.Rules.AbstractRule { 19 | public static metadata: Lint.IRuleMetadata = { 20 | ruleName: ruleName, 21 | type: 'functionality', 22 | description: 'Attributes of ion-item have been renamed.', 23 | options: null, 24 | optionsDescription: 'Not configurable.', 25 | typescriptOnly: false, 26 | hasFix: true 27 | }; 28 | 29 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 30 | return this.applyWithWalker( 31 | new NgWalker(sourceFile, this.getOptions(), { 32 | templateVisitorCtrl: IonItemAttributesRenamedTemplateVisitor 33 | }) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ionItemDividerIonLabelRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createIonLabelRequiredTemplateVisitorClass } from './helpers/ionLabelRequired'; 6 | 7 | export const ruleName = 'ion-item-divider-ion-label-required'; 8 | 9 | const TemplateVisitor = createIonLabelRequiredTemplateVisitorClass('ion-item-divider'); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'The ion-item-divider component requires an ion-label component. It is no longer automatically added.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: false, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker( 24 | new NgWalker(sourceFile, this.getOptions(), { 25 | templateVisitorCtrl: TemplateVisitor 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ionItemIonLabelRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createIonLabelRequiredTemplateVisitorClass } from './helpers/ionLabelRequired'; 6 | 7 | export const ruleName = 'ion-item-ion-label-required'; 8 | 9 | const TemplateVisitor = createIonLabelRequiredTemplateVisitorClass('ion-item'); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'The ion-item component requires an ion-label component. It is no longer automatically added.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: false, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker( 24 | new NgWalker(sourceFile, this.getOptions(), { 25 | templateVisitorCtrl: TemplateVisitor 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ionItemIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'ion-item'; 4 | 5 | export const ruleName = 'ion-item-is-now-an-element'; 6 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive); 7 | -------------------------------------------------------------------------------- /src/ionItemOptionIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as ts from 'typescript'; 6 | 7 | import { createDirectiveToElementTemplateVisitorClass } from './helpers/directiveToElement'; 8 | import { isElementAst } from './helpers/utils'; 9 | 10 | const directive = 'ion-button'; 11 | const description = 'Buttons within ion-item-options are now ion-item-option elements instead of Angular directives.'; 12 | 13 | export const ruleName = 'ion-item-option-is-now-an-element'; 14 | 15 | class TemplateVisitor extends BasicTemplateAstVisitor { 16 | visitElement(element: ast.ElementAst, context: any): any { 17 | if (element.name === 'ion-item-options') { 18 | for (const child of element.children) { 19 | if (isElementAst(child)) { 20 | if (child.name === 'button' && child.attrs.find(attr => attr.name === directive)) { 21 | const start = child.sourceSpan.start.offset + 1; 22 | 23 | this.addFailureAt(start, child.name.length, description); 24 | } 25 | } 26 | } 27 | } 28 | 29 | super.visitElement(element, context); 30 | } 31 | } 32 | 33 | export class Rule extends Lint.Rules.AbstractRule { 34 | public static metadata: Lint.IRuleMetadata = { 35 | ruleName: ruleName, 36 | type: 'functionality', 37 | description: description, 38 | options: null, 39 | optionsDescription: 'Not configurable.', 40 | typescriptOnly: false, 41 | hasFix: true 42 | }; 43 | 44 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 45 | return this.applyWithWalker( 46 | new NgWalker(sourceFile, this.getOptions(), { 47 | templateVisitorCtrl: TemplateVisitor 48 | }) 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ionItemOptionMarkupHasChangedRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as ts from 'typescript'; 6 | 7 | export const ruleName = 'ion-item-option-markup-has-changed'; 8 | export const ruleMessage = 'Inside of ion-item-options, ion-item-option must be used instead of ion-button.'; 9 | 10 | class IonItemOptionMarkupChangedTemplateVisitor extends BasicTemplateAstVisitor { 11 | visitElement(element: ast.ElementAst, context: any): any { 12 | if (element.name && element.name === 'ion-item-options') { 13 | element.children.forEach(child => { 14 | if ( 15 | (child instanceof ast.ElementAst && child.name === 'ion-button') || 16 | (child instanceof ast.ElementAst && child.name === 'button' && !!child.attrs.find(attr => attr.name === 'ion-button')) 17 | ) { 18 | const start = child.sourceSpan.start.offset; 19 | this.addFailure(this.createFailure(start + 1, child.name.length, ruleMessage)); 20 | } 21 | }); 22 | } 23 | super.visitElement(element, context); 24 | } 25 | } 26 | 27 | export class Rule extends Lint.Rules.AbstractRule { 28 | public static metadata: Lint.IRuleMetadata = { 29 | ruleName: ruleName, 30 | type: 'functionality', 31 | description: 'Buttons in ion-item-options have been renamed.', 32 | options: null, 33 | optionsDescription: 'Not configurable.', 34 | typescriptOnly: false, 35 | hasFix: true 36 | }; 37 | 38 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 39 | return this.applyWithWalker( 40 | new NgWalker(sourceFile, this.getOptions(), { 41 | templateVisitorCtrl: IonItemOptionMarkupChangedTemplateVisitor 42 | }) 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ionItemOptionMethodGetSlidingPercentRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as ts from 'typescript'; 3 | export const ruleName = 'ion-item-option-method-get-sliding-percent-renamed'; 4 | export const ruleMessage = '"getSlidingPercent" has been renamed to "getSlidingRatio"'; 5 | 6 | class GetSlidingPercentRenamedWalker extends Lint.RuleWalker { 7 | visitCallExpression(node: ts.CallExpression) { 8 | const expression = node.expression as any; 9 | if (expression.name && expression.name.text === 'getSlidingPercent') { 10 | const replacement = new Lint.Replacement(expression.name.getStart(), expression.name.getWidth(), 'getSlidingRatio'); 11 | this.addFailure(this.createFailure(expression.name.getStart(), expression.name.getWidth(), ruleMessage, replacement)); 12 | } 13 | } 14 | } 15 | 16 | export class Rule extends Lint.Rules.AbstractRule { 17 | public static metadata: Lint.IRuleMetadata = { 18 | ruleName: ruleName, 19 | type: 'functionality', 20 | description: 'getSlidingPercent is now called getSlidingRatio.', 21 | options: null, 22 | optionsDescription: 'Not configurable.', 23 | typescriptOnly: true, 24 | hasFix: true 25 | }; 26 | 27 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 28 | return this.applyWithWalker(new GetSlidingPercentRenamedWalker(sourceFile, this.getOptions())); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/ionItemOptionsAttributeValuesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributeValuesRenamedTemplateVisitorClass } from './helpers/attributeValuesRenamed'; 6 | 7 | export const ruleName = 'ion-item-options-attribute-values-renamed'; 8 | 9 | const replacementMap = new Map([['side', new Map([['left', 'start'], ['right', 'end']])]]); 10 | 11 | const TemplateVisitor = createAttributeValuesRenamedTemplateVisitorClass(['ion-item-options'], replacementMap); 12 | 13 | export class Rule extends Lint.Rules.AbstractRule { 14 | public static metadata: Lint.IRuleMetadata = { 15 | ruleName: ruleName, 16 | type: 'functionality', 17 | description: `Attribute values of ion-item-options have been renamed.`, 18 | options: null, 19 | optionsDescription: 'Not configurable.', 20 | typescriptOnly: false, 21 | hasFix: true 22 | }; 23 | 24 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 25 | return this.applyWithWalker( 26 | new NgWalker(sourceFile, this.getOptions(), { 27 | templateVisitorCtrl: TemplateVisitor 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ionLabelAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-label-attributes-renamed'; 8 | 9 | const replacementMap = new Map([['fixed', 'position="fixed"'], ['floating', 'position="floating"'], ['stacked', 'position="stacked"']]); 10 | 11 | const IonButtonAttributesAreRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-label'], replacementMap); 12 | 13 | export class Rule extends Lint.Rules.AbstractRule { 14 | public static metadata: Lint.IRuleMetadata = { 15 | ruleName: ruleName, 16 | type: 'functionality', 17 | description: 'Attributes of ion-label have been renamed.', 18 | options: null, 19 | optionsDescription: 'Not configurable.', 20 | typescriptOnly: false, 21 | hasFix: true 22 | }; 23 | 24 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 25 | return this.applyWithWalker( 26 | new NgWalker(sourceFile, this.getOptions(), { 27 | templateVisitorCtrl: IonButtonAttributesAreRenamedTemplateVisitor 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ionListHeaderIonLabelRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createIonLabelRequiredTemplateVisitorClass } from './helpers/ionLabelRequired'; 6 | 7 | export const ruleName = 'ion-list-header-ion-label-required'; 8 | 9 | const TemplateVisitor = createIonLabelRequiredTemplateVisitorClass('ion-list-header'); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'The ion-list-header component requires an ion-label component. It is no longer automatically added.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: false, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker( 24 | new NgWalker(sourceFile, this.getOptions(), { 25 | templateVisitorCtrl: TemplateVisitor 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ionLoadingMethodCreateParametersRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as ts from 'typescript'; 3 | import { createParametersRenamedClass } from './helpers/parametersRenamed'; 4 | 5 | export const ruleName = 'ion-loading-method-create-parameters-renamed'; 6 | 7 | const parameterMap = new Map([['content', 'message']]); 8 | const Walker = createParametersRenamedClass('create', 'LoadingController', parameterMap); 9 | 10 | export class Rule extends Lint.Rules.AbstractRule { 11 | public static metadata: Lint.IRuleMetadata = { 12 | ruleName: ruleName, 13 | type: 'functionality', 14 | description: 'LoadingController now takes in different parameters to its create method.', 15 | options: null, 16 | optionsDescription: 'Not configurable.', 17 | typescriptOnly: true, 18 | hasFix: true 19 | }; 20 | 21 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 22 | return this.applyWithWalker(new Walker(sourceFile, this.getOptions())); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ionMenuEventsRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributeEventsRenamed'; 6 | 7 | export const ruleName = 'ion-menu-events-renamed'; 8 | 9 | const replacementMap = new Map([['ionOpen', 'ionDidOpen'], ['ionClose', 'ionDidClose']]); 10 | 11 | const TemplateVisitor = createAttributesRenamedTemplateVisitorClass(undefined, replacementMap); 12 | 13 | export class Rule extends Lint.Rules.AbstractRule { 14 | public static metadata: Lint.IRuleMetadata = { 15 | ruleName: ruleName, 16 | type: 'functionality', 17 | description: 'Events for ion-menu have changed', 18 | options: null, 19 | optionsDescription: 'Not configurable.', 20 | typescriptOnly: false, 21 | hasFix: true 22 | }; 23 | 24 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 25 | return this.applyWithWalker( 26 | new NgWalker(sourceFile, this.getOptions(), { 27 | templateVisitorCtrl: TemplateVisitor 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ionMenuToggleIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'menuToggle'; 4 | 5 | export const ruleName = 'ion-menu-toggle-is-now-an-element'; 6 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-menu-toggle', 'parent'); 7 | -------------------------------------------------------------------------------- /src/ionNavbarIsNowIonToolbarRule.ts: -------------------------------------------------------------------------------- 1 | import { createElementRenameRuleClass } from './helpers/elementRename'; 2 | 3 | export const ruleName = 'ion-navbar-is-now-ion-toolbar'; 4 | 5 | export const Rule = createElementRenameRuleClass(ruleName, 'ion-navbar', 'ion-toolbar'); 6 | -------------------------------------------------------------------------------- /src/ionOptionIsNowIonSelectOptionRule.ts: -------------------------------------------------------------------------------- 1 | import { createElementRenameRuleClass } from './helpers/elementRename'; 2 | 3 | export const ruleName = 'ion-option-is-now-ion-select-option'; 4 | 5 | export const Rule = createElementRenameRuleClass(ruleName, 'ion-option', 'ion-select-option'); 6 | -------------------------------------------------------------------------------- /src/ionOverlayMethodCreateShouldUseAwaitRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as tsutils from 'tsutils'; 3 | import * as ts from 'typescript'; 4 | import { isValidForRule } from './helpers/parametersRenamed'; 5 | export const ruleName = 'ion-overlay-method-create-should-use-await'; 6 | export const ruleMessage = `The create method of overlay controllers now returns a promise. 7 | Please ensure that you are handling this promise correctly.`; 8 | 9 | const matchingControllers = [ 10 | 'PopoverController', 11 | 'ModalController', 12 | 'ActionSheetController', 13 | 'LoadingController', 14 | 'ToastController', 15 | 'AlertController' 16 | ]; 17 | 18 | class CreateMethodShouldUseAwaitWalker extends Lint.RuleWalker { 19 | visitCallExpression(node: ts.CallExpression) { 20 | if (node.arguments.length > 0) { 21 | const firstArgument = node.arguments[0]; 22 | 23 | if (isValidForRule(node, 'create', ...matchingControllers) && tsutils.isObjectLiteralExpression(firstArgument)) { 24 | if ( 25 | !tsutils.isAwaitExpression(node.parent) && 26 | (!tsutils.isPropertyAccessExpression(node.parent) || 27 | !tsutils.isCallExpression(node.parent.parent) || 28 | !tsutils.isPropertyAccessExpression(node.parent.parent.expression) || 29 | node.parent.parent.expression.name.text !== 'then') 30 | ) { 31 | this.addFailureAtNode(node, ruleMessage); 32 | } 33 | } 34 | } 35 | 36 | super.visitCallExpression(node); 37 | } 38 | } 39 | 40 | export class Rule extends Lint.Rules.AbstractRule { 41 | public static metadata: Lint.IRuleMetadata = { 42 | ruleName: ruleName, 43 | type: 'functionality', 44 | description: 'You must await the create method for the following controllers: ' + matchingControllers.join(', '), 45 | options: null, 46 | optionsDescription: 'Not configurable.', 47 | typescriptOnly: true, 48 | hasFix: true 49 | }; 50 | 51 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 52 | return this.applyWithWalker(new CreateMethodShouldUseAwaitWalker(sourceFile, this.getOptions())); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ionOverlayMethodPresentShouldUseAwaitRule.ts: -------------------------------------------------------------------------------- 1 | import * as Lint from 'tslint'; 2 | import * as tsutils from 'tsutils'; 3 | import * as ts from 'typescript'; 4 | export const ruleName = 'ion-overlay-method-present-should-use-await'; 5 | export const ruleMessage = `The present method of overlay controllers now returns a promise. 6 | Please ensure that you are handling this promise correctly.`; 7 | 8 | const matchingControllers = [ 9 | 'PopoverController', 10 | 'ModalController', 11 | 'ActionSheetController', 12 | 'LoadingController', 13 | 'ToastController', 14 | 'AlertController' 15 | ]; 16 | 17 | class CreateMethodShouldUseAwaitWalker extends Lint.RuleWalker { 18 | visitCallExpression(node: ts.CallExpression) { 19 | let expression = node.expression; 20 | 21 | if (tsutils.isPropertyAccessExpression(expression) && expression.name.text === 'present') { 22 | if ( 23 | !tsutils.isAwaitExpression(node.parent) && 24 | (!tsutils.isPropertyAccessExpression(node.parent) || 25 | !tsutils.isCallExpression(node.parent.parent) || 26 | !tsutils.isPropertyAccessExpression(node.parent.parent.expression) || 27 | node.parent.parent.expression.name.text !== 'then') 28 | ) { 29 | this.addFailureAtNode(node, ruleMessage); 30 | } 31 | } 32 | 33 | super.visitCallExpression(node); 34 | } 35 | } 36 | 37 | export class Rule extends Lint.Rules.AbstractRule { 38 | public static metadata: Lint.IRuleMetadata = { 39 | ruleName: ruleName, 40 | type: 'functionality', 41 | description: 'You must await the present method for the following controllers: ' + matchingControllers.join(', '), 42 | options: null, 43 | optionsDescription: 'Not configurable.', 44 | typescriptOnly: true, 45 | hasFix: true 46 | }; 47 | 48 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 49 | return this.applyWithWalker(new CreateMethodShouldUseAwaitWalker(sourceFile, this.getOptions())); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ionRadioAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-radio-attributes-renamed'; 8 | 9 | const replacementMap = new Map([['item-left', 'slot="start"'], ['item-right', 'slot="end"']]); 10 | 11 | const IonFabAttributesRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-radio'], replacementMap); 12 | 13 | export class Rule extends Lint.Rules.AbstractRule { 14 | public static metadata: Lint.IRuleMetadata = { 15 | ruleName: ruleName, 16 | type: 'functionality', 17 | description: 'Attributes of ion-radio have been renamed.', 18 | options: null, 19 | optionsDescription: 'Not configurable.', 20 | typescriptOnly: false, 21 | hasFix: true 22 | }; 23 | 24 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 25 | return this.applyWithWalker( 26 | new NgWalker(sourceFile, this.getOptions(), { 27 | templateVisitorCtrl: IonFabAttributesRenamedTemplateVisitor 28 | }) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ionRadioGroupIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'radio-group'; 4 | 5 | export const ruleName = 'ion-radio-group-is-now-an-element'; 6 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-radio-group', 'child'); 7 | -------------------------------------------------------------------------------- /src/ionRadioSlotRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import * as ast from '@angular/compiler'; 2 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 3 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 4 | import * as Lint from 'tslint'; 5 | import * as ts from 'typescript'; 6 | 7 | export const ruleName = 'ion-radio-slot-required'; 8 | 9 | class TemplateVisitor extends BasicTemplateAstVisitor { 10 | visitElement(element: ast.ElementAst, context: any): any { 11 | if (element.name && element.name === 'ion-radio') { 12 | const attributeFound = element.attrs.find(attr => attr.name === 'slot'); 13 | 14 | if (!attributeFound) { 15 | const start = element.sourceSpan.start.offset; 16 | const length = element.name.length; 17 | const position = this.getSourcePosition(start) + length + 1; 18 | 19 | this.addFailureAt(start + 1, length, 'The slot attribute of ion-radio is required. Use slot="start".', [ 20 | Lint.Replacement.replaceFromTo(position, position, ' slot="start"') 21 | ]); 22 | } 23 | } 24 | 25 | super.visitElement(element, context); 26 | } 27 | } 28 | 29 | export class Rule extends Lint.Rules.AbstractRule { 30 | public static metadata: Lint.IRuleMetadata = { 31 | ruleName: ruleName, 32 | type: 'functionality', 33 | description: 'The slot attribute of ion-radio is now required.', 34 | options: null, 35 | optionsDescription: 'Not configurable.', 36 | typescriptOnly: false, 37 | hasFix: true 38 | }; 39 | 40 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 41 | return this.applyWithWalker( 42 | new NgWalker(sourceFile, this.getOptions(), { 43 | templateVisitorCtrl: TemplateVisitor 44 | }) 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ionRangeAttributesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributesRenamed'; 6 | 7 | export const ruleName = 'ion-range-attributes-renamed'; 8 | 9 | const replacementMap = new Map([ 10 | ['range-left', 'slot="start"'], 11 | ['range-start', 'slot="start"'], 12 | ['range-right', 'slot="end"'], 13 | ['range-end', 'slot="end"'] 14 | ]); 15 | 16 | const TemplateVisitor = createAttributesRenamedTemplateVisitorClass(undefined, replacementMap); 17 | 18 | export class Rule extends Lint.Rules.AbstractRule { 19 | public static metadata: Lint.IRuleMetadata = { 20 | ruleName: ruleName, 21 | type: 'functionality', 22 | description: 'Attributes of children of ion-range have been renamed.', 23 | options: null, 24 | optionsDescription: 'Not configurable.', 25 | typescriptOnly: false, 26 | hasFix: true 27 | }; 28 | 29 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 30 | return this.applyWithWalker( 31 | new NgWalker(sourceFile, this.getOptions(), { 32 | templateVisitorCtrl: TemplateVisitor 33 | }) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ionSegmentButtonIonLabelRequiredRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createIonLabelRequiredTemplateVisitorClass } from './helpers/ionLabelRequired'; 6 | 7 | export const ruleName = 'ion-segment-button-ion-label-required'; 8 | 9 | const TemplateVisitor = createIonLabelRequiredTemplateVisitorClass('ion-segment-button'); 10 | 11 | export class Rule extends Lint.Rules.AbstractRule { 12 | public static metadata: Lint.IRuleMetadata = { 13 | ruleName: ruleName, 14 | type: 'functionality', 15 | description: 'The ion-segment-button component requires an ion-label component. It is no longer automatically added.', 16 | options: null, 17 | optionsDescription: 'Not configurable.', 18 | typescriptOnly: false, 19 | hasFix: true 20 | }; 21 | 22 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 23 | return this.applyWithWalker( 24 | new NgWalker(sourceFile, this.getOptions(), { 25 | templateVisitorCtrl: TemplateVisitor 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ionSpinnerAttributeValuesRenamedRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import { createAttributeValuesRenamedTemplateVisitorClass } from './helpers/attributeValuesRenamed'; 6 | 7 | export const ruleName = 'ion-spinner-attribute-values-renamed'; 8 | 9 | const replacementMap = new Map([['name', new Map([['ios', 'lines'], ['ios-small', 'lines-small']])]]); 10 | const affectedElements = ['ion-spinner', 'ion-loading', 'ion-infinite-scroll', 'ion-refresher']; 11 | 12 | const TemplateVisitor = createAttributeValuesRenamedTemplateVisitorClass(affectedElements, replacementMap); 13 | 14 | export class Rule extends Lint.Rules.AbstractRule { 15 | public static metadata: Lint.IRuleMetadata = { 16 | ruleName: ruleName, 17 | type: 'functionality', 18 | description: `Attribute values of ${affectedElements.join(', ')} have been renamed.`, 19 | options: null, 20 | optionsDescription: 'Not configurable.', 21 | typescriptOnly: false, 22 | hasFix: true 23 | }; 24 | 25 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 26 | return this.applyWithWalker( 27 | new NgWalker(sourceFile, this.getOptions(), { 28 | templateVisitorCtrl: TemplateVisitor 29 | }) 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ionTabsRefactoredRule.ts: -------------------------------------------------------------------------------- 1 | import { NgWalker } from 'codelyzer/angular/ngWalker'; 2 | import * as Lint from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | import * as ast from '@angular/compiler'; 6 | import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; 7 | import { MetadataReader } from 'codelyzer/angular/metadataReader'; 8 | 9 | export const ruleName = 'ion-tabs-refactored'; 10 | 11 | const TABS = 'ion-tabs'; 12 | 13 | const TemplateVisitor = createTabsRefactoredVisitorClass(TABS); 14 | 15 | export class Rule extends Lint.Rules.AbstractRule { 16 | public static metadata: Lint.IRuleMetadata = { 17 | ruleName: ruleName, 18 | type: 'functionality', 19 | description: 'Tabs have been refacotred, please see this blog post: ', 20 | options: null, 21 | optionsDescription: 'Not configurable.', 22 | typescriptOnly: false, 23 | hasFix: false 24 | }; 25 | public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { 26 | return this.applyWithWalker( 27 | new NgWalker(sourceFile, this.getOptions(), { 28 | templateVisitorCtrl: TemplateVisitor 29 | }) 30 | ); 31 | } 32 | } 33 | 34 | function generateErrorMessage() { 35 | return `Tabs have gone through a significant refactor. 36 | Please see https://github.com/ionic-team/ionic/blob/master/CHANGELOG.md#angular-tabs`; 37 | } 38 | 39 | export function createTabsRefactoredVisitorClass(tabElement: string) { 40 | return class extends BasicTemplateAstVisitor { 41 | visitElement(element: ast.ElementAst, context: any): any { 42 | if (element.name === tabElement) this.checkElement(element); 43 | super.visitElement(element, context); 44 | } 45 | 46 | private checkElement(element: ast.ElementAst) { 47 | const start = element.sourceSpan.start.offset + 1; 48 | const length = element.name.length; 49 | this.addFailureAt(start, length, generateErrorMessage()); 50 | } 51 | }; 52 | } 53 | -------------------------------------------------------------------------------- /src/ionTextIsNowAnElementRule.ts: -------------------------------------------------------------------------------- 1 | import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'; 2 | 3 | const directive = 'ion-text'; 4 | 5 | export const ruleName = 'ion-text-is-now-an-element'; 6 | export const Rule = createDirectiveToElementRuleClass(ruleName, directive, undefined, 'parent'); 7 | -------------------------------------------------------------------------------- /test/ionActionSheetMethodCreateParametersRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionActionSheetMethodCreateParametersRenamedRule'; 4 | import { assertAnnotated, assertFailures, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | class DoSomething{ 11 | constructor(private actionSheetCtrl: ActionSheetController){} 12 | 13 | showAlert(){ 14 | const actionSheet = await this.actionSheetCtrl.create({ 15 | header: 'This is the title', 16 | subHeader: 'this is the sub title' 17 | }); 18 | await actionSheet.present(); 19 | } 20 | } 21 | `; 22 | assertSuccess(ruleName, source); 23 | }); 24 | 25 | it('should not be triggered if the type is not ActionSheetController', () => { 26 | let source = ` 27 | class DoSomething{ 28 | constructor(private actionSheetCtrl: SomeOtherController){} 29 | 30 | showActionSheet(){ 31 | const actionSheet = await this.actionSheetCtrl.create({ 32 | title: 'This is the title', 33 | subTitle: 'this is the sub title' 34 | }); 35 | await actionSheet.present(); 36 | } 37 | } 38 | `; 39 | assertSuccess(ruleName, source); 40 | }); 41 | 42 | it('should work with different names for the ActionSheetController object', () => { 43 | let source = ` 44 | class DoSomething{ 45 | constructor(private myOtherNamedCtrl: ActionSheetController){} 46 | 47 | showAlert(){ 48 | const actionSheet = await this.myOtherNamedCtrl.create({ 49 | header: 'This is the title', 50 | subHeader: 'this is the sub title' 51 | }); 52 | await actionSheet.present(); 53 | } 54 | } 55 | `; 56 | assertSuccess(ruleName, source); 57 | }); 58 | }); 59 | 60 | describe('failure', () => { 61 | it('should fail when title is passed in', () => { 62 | let source = ` 63 | class DoSomething{ 64 | constructor(private actionSheetCtrl: ActionSheetController){} 65 | 66 | showAlert(){ 67 | const actionSheet = await this.actionSheetCtrl.create({ 68 | title: 'This is the title', 69 | ~~~~~ 70 | subTitle: 'this is the sub title' 71 | }); 72 | await actionSheet.present(); 73 | } 74 | } 75 | `; 76 | 77 | assertAnnotated({ 78 | ruleName, 79 | message: 'Property title has been renamed to header.', 80 | source 81 | }); 82 | }); 83 | 84 | it('should fail when subTitle is passed in', () => { 85 | let source = ` 86 | class DoSomething{ 87 | constructor(private actionSheetCtrl: ActionSheetController){} 88 | 89 | showAlert(){ 90 | const actionSheet = await this.actionSheetCtrl.create({ 91 | header: 'This is the title', 92 | subTitle: 'this is the sub title' 93 | ~~~~~~~~ 94 | }); 95 | await actionSheet.present(); 96 | } 97 | } 98 | `; 99 | 100 | assertAnnotated({ 101 | ruleName, 102 | message: 'Property subTitle has been renamed to subHeader.', 103 | source 104 | }); 105 | }); 106 | }); 107 | 108 | describe('replacements', () => { 109 | it('should replace multiple', () => { 110 | let source = ` 111 | class DoSomething{ 112 | constructor(private actionSheetCtrl: ActionSheetController){} 113 | 114 | showAlert(){ 115 | const actionSheet = await this.actionSheetCtrl.create({ 116 | title: 'This is the title', 117 | subTitle: 'this is the sub title' 118 | }); 119 | await actionSheet.present(); 120 | } 121 | } 122 | `; 123 | 124 | const failures = assertFailures(ruleName, source, [ 125 | { 126 | message: 'Property title has been renamed to header.', 127 | startPosition: { 128 | line: 6, 129 | character: 12 130 | }, 131 | endPosition: { 132 | line: 6, 133 | character: 17 134 | } 135 | }, 136 | { 137 | message: 'Property subTitle has been renamed to subHeader.', 138 | startPosition: { 139 | line: 7, 140 | character: 12 141 | }, 142 | endPosition: { 143 | line: 7, 144 | character: 20 145 | } 146 | } 147 | ]); 148 | 149 | const fixes = failures.map(f => f.getFix()); 150 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 151 | 152 | expect(res).to.eq(` 153 | class DoSomething{ 154 | constructor(private actionSheetCtrl: ActionSheetController){} 155 | 156 | showAlert(){ 157 | const actionSheet = await this.actionSheetCtrl.create({ 158 | header: 'This is the title', 159 | subHeader: 'this is the sub title' 160 | }); 161 | await actionSheet.present(); 162 | } 163 | } 164 | `); 165 | }); 166 | 167 | it('should replace shorthand property values', () => { 168 | let source = ` 169 | class DoSomething{ 170 | constructor(private actionSheetCtrl: ActionSheetController){} 171 | 172 | const title = 'This is the title' 173 | 174 | showAlert(){ 175 | const actionSheet = await this.actionSheetCtrl.create({ 176 | title, 177 | subTitle: 'this is the sub title' 178 | }); 179 | await actionSheet.present(); 180 | } 181 | } 182 | `; 183 | 184 | const failures = assertFailures(ruleName, source, [ 185 | { 186 | message: 'Property title has been renamed to header.', 187 | startPosition: { 188 | line: 8, 189 | character: 12 190 | }, 191 | endPosition: { 192 | line: 8, 193 | character: 17 194 | } 195 | }, 196 | { 197 | message: 'Property subTitle has been renamed to subHeader.', 198 | startPosition: { 199 | line: 9, 200 | character: 12 201 | }, 202 | endPosition: { 203 | line: 9, 204 | character: 20 205 | } 206 | } 207 | ]); 208 | 209 | const fixes = failures.map(f => f.getFix()); 210 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 211 | 212 | expect(res).to.eq(` 213 | class DoSomething{ 214 | constructor(private actionSheetCtrl: ActionSheetController){} 215 | 216 | const title = 'This is the title' 217 | 218 | showAlert(){ 219 | const actionSheet = await this.actionSheetCtrl.create({ 220 | header: title, 221 | subHeader: 'this is the sub title' 222 | }); 223 | await actionSheet.present(); 224 | } 225 | } 226 | `); 227 | }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /test/ionAlertMethodCreateParametersRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionAlertMethodCreateParametersRenamedRule'; 4 | import { assertAnnotated, assertFailures, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | class DoSomething{ 11 | constructor(private alertCtrl: AlertController){} 12 | 13 | showAlert(){ 14 | const alert = await this.alertCtrl.create({ 15 | header: 'This is the title', 16 | subHeader: 'this is the sub title' 17 | }); 18 | await alert.present(); 19 | } 20 | } 21 | `; 22 | assertSuccess(ruleName, source); 23 | }); 24 | 25 | it('should not be triggered if the type is not AlertController', () => { 26 | let source = ` 27 | class DoSomething{ 28 | constructor(private actionSheetCtrl: SomeOtherController){} 29 | 30 | showActionSheet(){ 31 | const actionSheet = await this.actionSheetCtrl.create({ 32 | title: 'This is the title', 33 | subTitle: 'this is the sub title' 34 | }); 35 | await actionSheet.present(); 36 | } 37 | } 38 | `; 39 | assertSuccess(ruleName, source); 40 | }); 41 | 42 | it('should work with different names for the AlertController object', () => { 43 | let source = ` 44 | class DoSomething{ 45 | constructor(private myOtherNamedCtrl: AlertController){} 46 | 47 | showAlert(){ 48 | const alert = await this.myOtherNamedCtrl.create({ 49 | header: 'This is the title', 50 | subHeader: 'this is the sub title' 51 | }); 52 | await alert.present(); 53 | } 54 | } 55 | `; 56 | assertSuccess(ruleName, source); 57 | }); 58 | }); 59 | 60 | describe('failure', () => { 61 | it('should fail when title is passed in', () => { 62 | let source = ` 63 | class DoSomething{ 64 | constructor(private alertCtrl: AlertController){} 65 | 66 | showAlert(){ 67 | const alert = await this.alertCtrl.create({ 68 | title: 'This is the title', 69 | ~~~~~ 70 | subTitle: 'this is the sub title' 71 | }); 72 | await alert.present(); 73 | } 74 | } 75 | `; 76 | 77 | assertAnnotated({ 78 | ruleName, 79 | message: 'Property title has been renamed to header.', 80 | source 81 | }); 82 | }); 83 | 84 | it('should fail when subTitle is passed in', () => { 85 | let source = ` 86 | class DoSomething{ 87 | constructor(private alertCtrl: AlertController){} 88 | 89 | showAlert(){ 90 | const alert = await this.alertCtrl.create({ 91 | header: 'This is the title', 92 | subTitle: 'this is the sub title' 93 | ~~~~~~~~ 94 | }); 95 | await alert.present(); 96 | } 97 | } 98 | `; 99 | 100 | assertAnnotated({ 101 | ruleName, 102 | message: 'Property subTitle has been renamed to subHeader.', 103 | source 104 | }); 105 | }); 106 | }); 107 | 108 | describe('replacements', () => { 109 | it('should replace multiple', () => { 110 | let source = ` 111 | class DoSomething{ 112 | constructor(private alertCtrl: AlertController){} 113 | 114 | showAlert(){ 115 | const alert = await this.alertCtrl.create({ 116 | title: 'This is the title', 117 | subTitle: 'this is the sub title' 118 | }); 119 | await alert.present(); 120 | } 121 | } 122 | `; 123 | 124 | const failures = assertFailures(ruleName, source, [ 125 | { 126 | message: 'Property title has been renamed to header.', 127 | startPosition: { 128 | line: 6, 129 | character: 12 130 | }, 131 | endPosition: { 132 | line: 6, 133 | character: 17 134 | } 135 | }, 136 | { 137 | message: 'Property subTitle has been renamed to subHeader.', 138 | startPosition: { 139 | line: 7, 140 | character: 12 141 | }, 142 | endPosition: { 143 | line: 7, 144 | character: 20 145 | } 146 | } 147 | ]); 148 | 149 | const fixes = failures.map(f => f.getFix()); 150 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 151 | 152 | expect(res).to.eq(` 153 | class DoSomething{ 154 | constructor(private alertCtrl: AlertController){} 155 | 156 | showAlert(){ 157 | const alert = await this.alertCtrl.create({ 158 | header: 'This is the title', 159 | subHeader: 'this is the sub title' 160 | }); 161 | await alert.present(); 162 | } 163 | } 164 | `); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /test/ionBackButtonNotAddedByDefault.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionBackButtonNotAddedByDefaultRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with proper style', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | 13 | 14 | Back Button Example 15 | 16 | \` 17 | }) 18 | class Bar{} 19 | `; 20 | assertSuccess(ruleName, source); 21 | }); 22 | }); 23 | 24 | describe('failure', () => { 25 | it('should fail when ion-buttons is missing', () => { 26 | let source = ` 27 | @Component({ 28 | template: \` 29 | 30 | ~~~~~~~~~~~ 31 | Back Button Example 32 | 33 | \` 34 | }) 35 | class Bar{} 36 | `; 37 | 38 | assertAnnotated({ 39 | ruleName, 40 | message: 'The back button in an ion-toolbar is no longer automatically added.', 41 | source 42 | }); 43 | }); 44 | 45 | it('should fail when ion-back-button is missing in ion-buttons', () => { 46 | let source = ` 47 | @Component({ 48 | template: \` 49 | 50 | ~~~~~~~~~~~ 51 | 52 | 53 | Back Button Example 54 | 55 | \` 56 | }) 57 | class Bar{} 58 | `; 59 | 60 | assertAnnotated({ 61 | ruleName, 62 | message: 'The back button in an ion-toolbar is no longer automatically added.', 63 | source 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/ionButtonIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName } from '../src/ionButtonIsNowAnElementRule'; 3 | import { assertAnnotated, assertSuccess, assertFailure } from './testHelper'; 4 | import { Utils, Replacement } from 'tslint'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when ion-button attribute is used on button', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'ion-button is now an ion-button element instead of an Angular directive.', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail when ion-button attribute is used on anchor', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'ion-button is now an ion-button element instead of an Angular directive.', 50 | source 51 | }); 52 | }); 53 | 54 | it('should fail when ion-button attribute is used with multiline', () => { 55 | let source = ` 56 | @Component({ 57 | template: \` 58 | Click Me\` 62 | }) 63 | class Bar{} 64 | `; 65 | 66 | assertAnnotated({ 67 | ruleName, 68 | message: 'ion-button is now an ion-button element instead of an Angular directive.', 69 | source 70 | }); 71 | }); 72 | }); 73 | 74 | describe('replacements', () => { 75 | it('should replace button with ion-button and remove attribute', () => { 76 | let source = ` 77 | @Component({ 78 | template: \` 79 | \` 80 | }) 81 | class Bar {} 82 | `; 83 | const fail = { 84 | message: 'ion-button is now an ion-button element instead of an Angular directive.', 85 | startPosition: { 86 | line: 2, 87 | character: 29 88 | }, 89 | endPosition: { 90 | line: 2, 91 | character: 39 92 | } 93 | }; 94 | 95 | const failures = assertFailure(ruleName, source, fail); 96 | const fixes = failures.map(f => f.getFix()); 97 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 98 | 99 | let expected = ` 100 | @Component({ 101 | template: \` 102 | \` 103 | }) 104 | class Bar {} 105 | `; 106 | expect(res).to.eq(expected); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/ionButtonsAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionButtonsAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | Secondary 14 | 15 | \` 16 | }) 17 | class Bar{} 18 | `; 19 | assertSuccess(ruleName, source); 20 | }); 21 | }); 22 | 23 | describe('failure', () => { 24 | it('should fail when start is used', () => { 25 | let source = ` 26 | @Component({ 27 | template: \` 28 | 29 | ~~~~~ 30 | Secondary 31 | 32 | \` 33 | }) 34 | class Bar{} 35 | `; 36 | 37 | assertAnnotated({ 38 | ruleName, 39 | message: 'The start attribute of ion-buttons has been renamed. Use slot="secondary" instead.', 40 | source 41 | }); 42 | }); 43 | 44 | it('should fail when end is used', () => { 45 | let source = ` 46 | @Component({ 47 | template: \` 48 | 49 | ~~~ 50 | Primary 51 | 52 | \` 53 | }) 54 | class Bar{} 55 | `; 56 | 57 | assertAnnotated({ 58 | ruleName, 59 | message: 'The end attribute of ion-buttons has been renamed. Use slot="primary" instead.', 60 | source 61 | }); 62 | }); 63 | 64 | it('should fail when left is used', () => { 65 | let source = ` 66 | @Component({ 67 | template: \` 68 | 69 | ~~~~ 70 | Left 71 | 72 | \` 73 | }) 74 | class Bar{} 75 | `; 76 | 77 | assertAnnotated({ 78 | ruleName, 79 | message: 'The left attribute of ion-buttons has been renamed. Use slot="start" instead.', 80 | source 81 | }); 82 | }); 83 | 84 | it('should fail when right is used', () => { 85 | let source = ` 86 | @Component({ 87 | template: \` 88 | 89 | ~~~~~ 90 | Right 91 | 92 | \` 93 | }) 94 | class Bar{} 95 | `; 96 | 97 | assertAnnotated({ 98 | ruleName, 99 | message: 'The right attribute of ion-buttons has been renamed. Use slot="end" instead.', 100 | source 101 | }); 102 | }); 103 | }); 104 | 105 | describe('replacements', () => { 106 | it('should replace start with slot="secondary"', () => { 107 | let source = ` 108 | @Component({ 109 | template: \` 110 | 111 | Secondary 112 | 113 | \` 114 | }) 115 | class Bar {} 116 | `; 117 | 118 | const fail = { 119 | message: 'The start attribute of ion-buttons has been renamed. Use slot="secondary" instead.', 120 | startPosition: { 121 | line: 3, 122 | character: 25 123 | }, 124 | endPosition: { 125 | line: 3, 126 | character: 30 127 | } 128 | }; 129 | 130 | const failures = assertFailure(ruleName, source, fail); 131 | const fixes = failures.map(f => f.getFix()); 132 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 133 | 134 | let expected = ` 135 | @Component({ 136 | template: \` 137 | 138 | Secondary 139 | 140 | \` 141 | }) 142 | class Bar {} 143 | `; 144 | 145 | expect(res).to.eq(expected); 146 | }); 147 | 148 | it('should replace end with slot="primary"', () => { 149 | let source = ` 150 | @Component({ 151 | template: \` 152 | 153 | Primary 154 | 155 | \` 156 | }) 157 | class Bar {} 158 | `; 159 | 160 | const fail = { 161 | message: 'The end attribute of ion-buttons has been renamed. Use slot="primary" instead.', 162 | startPosition: { 163 | line: 3, 164 | character: 25 165 | }, 166 | endPosition: { 167 | line: 3, 168 | character: 28 169 | } 170 | }; 171 | 172 | const failures = assertFailure(ruleName, source, fail); 173 | const fixes = failures.map(f => f.getFix()); 174 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 175 | 176 | let expected = ` 177 | @Component({ 178 | template: \` 179 | 180 | Primary 181 | 182 | \` 183 | }) 184 | class Bar {} 185 | `; 186 | 187 | expect(res).to.eq(expected); 188 | }); 189 | 190 | it('should replace left with slot="start"', () => { 191 | let source = ` 192 | @Component({ 193 | template: \` 194 | 195 | Left 196 | 197 | \` 198 | }) 199 | class Bar {} 200 | `; 201 | 202 | const fail = { 203 | message: 'The left attribute of ion-buttons has been renamed. Use slot="start" instead.', 204 | startPosition: { 205 | line: 3, 206 | character: 25 207 | }, 208 | endPosition: { 209 | line: 3, 210 | character: 29 211 | } 212 | }; 213 | 214 | const failures = assertFailure(ruleName, source, fail); 215 | const fixes = failures.map(f => f.getFix()); 216 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 217 | 218 | let expected = ` 219 | @Component({ 220 | template: \` 221 | 222 | Left 223 | 224 | \` 225 | }) 226 | class Bar {} 227 | `; 228 | 229 | expect(res).to.eq(expected); 230 | }); 231 | 232 | it('should replace right with slot="end"', () => { 233 | let source = ` 234 | @Component({ 235 | template: \` 236 | 237 | Right 238 | 239 | \` 240 | }) 241 | class Bar {} 242 | `; 243 | 244 | const fail = { 245 | message: 'The right attribute of ion-buttons has been renamed. Use slot="end" instead.', 246 | startPosition: { 247 | line: 3, 248 | character: 25 249 | }, 250 | endPosition: { 251 | line: 3, 252 | character: 30 253 | } 254 | }; 255 | 256 | const failures = assertFailure(ruleName, source, fail); 257 | const fixes = failures.map(f => f.getFix()); 258 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 259 | 260 | let expected = ` 261 | @Component({ 262 | template: \` 263 | 264 | Right 265 | 266 | \` 267 | }) 268 | class Bar {} 269 | `; 270 | 271 | expect(res).to.eq(expected); 272 | }); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /test/ionColAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionColAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper size attribute', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar {} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | 18 | it('should work with proper size attribute', () => { 19 | let source = ` 20 | @Component({ 21 | template: \`\` 22 | }) 23 | class Bar {} 24 | `; 25 | assertSuccess(ruleName, source); 26 | }); 27 | 28 | it('should work with proper offset attribute', () => { 29 | let source = ` 30 | @Component({ 31 | template: \`\` 32 | }) 33 | class Bar {} 34 | `; 35 | assertSuccess(ruleName, source); 36 | }); 37 | }); 38 | 39 | describe('failure', () => { 40 | it('should fail when col-3 is used', () => { 41 | let source = ` 42 | @Component({ 43 | template: \` 44 | 45 | ~~~~~ 46 | 47 | \` 48 | }) 49 | class Bar {} 50 | `; 51 | 52 | assertAnnotated({ 53 | ruleName, 54 | message: 'The col-3 attribute of ion-col has been renamed. Use size="3" instead.', 55 | source 56 | }); 57 | }); 58 | 59 | it('should fail when col-xs-3 is used', () => { 60 | let source = ` 61 | @Component({ 62 | template: \` 63 | 64 | ~~~~~~~~ 65 | 66 | \` 67 | }) 68 | class Bar {} 69 | `; 70 | 71 | assertAnnotated({ 72 | ruleName, 73 | message: 'The col-xs-3 attribute of ion-col has been renamed. Use size-xs="3" instead.', 74 | source 75 | }); 76 | }); 77 | 78 | it('should fail when col-auto is used', () => { 79 | let source = ` 80 | @Component({ 81 | template: \` 82 | 83 | ~~~~~~~~ 84 | 85 | \` 86 | }) 87 | class Bar {} 88 | `; 89 | 90 | assertAnnotated({ 91 | ruleName, 92 | message: 'The col-auto attribute of ion-col has been renamed. Use size="auto" instead.', 93 | source 94 | }); 95 | }); 96 | 97 | it('should fail when col-xs-auto is used', () => { 98 | let source = ` 99 | @Component({ 100 | template: \` 101 | 102 | ~~~~~~~~~~~ 103 | 104 | \` 105 | }) 106 | class Bar {} 107 | `; 108 | 109 | assertAnnotated({ 110 | ruleName, 111 | message: 'The col-xs-auto attribute of ion-col has been renamed. Use size-xs="auto" instead.', 112 | source 113 | }); 114 | }); 115 | }); 116 | 117 | describe('replacements', () => { 118 | it('should replace col-3 with size="3"', () => { 119 | let source = ` 120 | @Component({ 121 | template: \` 122 | 123 | \` 124 | }) 125 | class Bar {} 126 | `; 127 | 128 | const fail = { 129 | message: 'The col-3 attribute of ion-col has been renamed. Use size="3" instead.', 130 | startPosition: { 131 | line: 3, 132 | character: 21 133 | }, 134 | endPosition: { 135 | line: 3, 136 | character: 26 137 | } 138 | }; 139 | 140 | const failures = assertFailure(ruleName, source, fail); 141 | const fixes = failures.map(f => f.getFix()); 142 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 143 | 144 | let expected = ` 145 | @Component({ 146 | template: \` 147 | 148 | \` 149 | }) 150 | class Bar {} 151 | `; 152 | 153 | expect(res).to.eq(expected); 154 | }); 155 | 156 | it('should replace col-auto with size="auto"', () => { 157 | let source = ` 158 | @Component({ 159 | template: \` 160 | 161 | \` 162 | }) 163 | class Bar {} 164 | `; 165 | 166 | const fail = { 167 | message: 'The col-auto attribute of ion-col has been renamed. Use size="auto" instead.', 168 | startPosition: { 169 | line: 3, 170 | character: 21 171 | }, 172 | endPosition: { 173 | line: 3, 174 | character: 29 175 | } 176 | }; 177 | 178 | const failures = assertFailure(ruleName, source, fail); 179 | const fixes = failures.map(f => f.getFix()); 180 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 181 | 182 | let expected = ` 183 | @Component({ 184 | template: \` 185 | 186 | \` 187 | }) 188 | class Bar {} 189 | `; 190 | 191 | expect(res).to.eq(expected); 192 | }); 193 | 194 | it('should replace col-xs-3 with size-xs="3"', () => { 195 | let source = ` 196 | @Component({ 197 | template: \` 198 | 199 | \` 200 | }) 201 | class Bar {} 202 | `; 203 | 204 | const fail = { 205 | message: 'The col-xs-3 attribute of ion-col has been renamed. Use size-xs="3" instead.', 206 | startPosition: { 207 | line: 3, 208 | character: 21 209 | }, 210 | endPosition: { 211 | line: 3, 212 | character: 29 213 | } 214 | }; 215 | 216 | const failures = assertFailure(ruleName, source, fail); 217 | const fixes = failures.map(f => f.getFix()); 218 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 219 | 220 | let expected = ` 221 | @Component({ 222 | template: \` 223 | 224 | \` 225 | }) 226 | class Bar {} 227 | `; 228 | 229 | expect(res).to.eq(expected); 230 | }); 231 | 232 | it('should replace col-xs-auto with size-xs="auto"', () => { 233 | let source = ` 234 | @Component({ 235 | template: \` 236 | 237 | \` 238 | }) 239 | class Bar {} 240 | `; 241 | 242 | const fail = { 243 | message: 'The col-xs-auto attribute of ion-col has been renamed. Use size-xs="auto" instead.', 244 | startPosition: { 245 | line: 3, 246 | character: 21 247 | }, 248 | endPosition: { 249 | line: 3, 250 | character: 32 251 | } 252 | }; 253 | 254 | const failures = assertFailure(ruleName, source, fail); 255 | const fixes = failures.map(f => f.getFix()); 256 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 257 | 258 | let expected = ` 259 | @Component({ 260 | template: \` 261 | 262 | \` 263 | }) 264 | class Bar {} 265 | `; 266 | 267 | expect(res).to.eq(expected); 268 | }); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /test/ionDatetimeCapitalizationChanged.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionDatetimeCapitalizationChangedRule'; 2 | import { assertAnnotated, assertSuccess, assertMultipleAnnotated } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work when the import and path are correct', () => { 7 | let source = ` 8 | import {Datetime} from '@ionic/angular'; 9 | `; 10 | assertSuccess(ruleName, source); 11 | }); 12 | }); 13 | describe('failure', () => { 14 | it('should fail when symbol is right, but path is wrong', () => { 15 | let source = ` 16 | import {Datetime} from 'ionic-angular'; 17 | ~~~~~~~~~~~~~ 18 | `; 19 | assertAnnotated({ 20 | ruleName, 21 | source, 22 | message: 'outdated import path' 23 | }); 24 | }); 25 | it('should fail when symbol is wrong, but path is right', () => { 26 | let source = ` 27 | import {DateTime} from '@ionic/angular'; 28 | ~~~~~~~~ 29 | `; 30 | assertAnnotated({ 31 | ruleName, 32 | source, 33 | message: 'imported symbol no longer exists' 34 | }); 35 | }); 36 | it('should fail when symbol and path are wrong', () => { 37 | let source = ` 38 | import {DateTime} from 'ionic-angular'; 39 | ~~~~~~~~ ^^^^^^^^^^^^^ 40 | `; 41 | assertMultipleAnnotated({ 42 | ruleName, 43 | source, 44 | failures: [ 45 | { 46 | char: '~', 47 | msg: 'imported symbol no longer exists' 48 | }, 49 | { 50 | char: '^', 51 | msg: 'outdated import path' 52 | } 53 | ] 54 | }); 55 | }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/ionFabButtonIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName } from '../src/ionFabButtonIsNowAnElementRule'; 3 | import { assertAnnotated, assertSuccess, assertFailure } from './testHelper'; 4 | import { Replacement, Utils } from 'tslint'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should not trigger for parent ion-fab', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | \` 20 | }) 21 | class Bar{} 22 | `; 23 | assertSuccess(ruleName, source); 24 | }); 25 | 26 | it('should work with proper style', () => { 27 | let source = ` 28 | @Component({ 29 | template: \`\` 30 | }) 31 | class Bar{} 32 | `; 33 | assertSuccess(ruleName, source); 34 | }); 35 | }); 36 | 37 | describe('failure', () => { 38 | it('should fail when ion-fab attribute is used on button', () => { 39 | let source = ` 40 | @Component({ 41 | template: \` 42 | \` 43 | ~~~~~~~ 44 | }) 45 | class Bar{} 46 | `; 47 | 48 | assertAnnotated({ 49 | ruleName, 50 | message: 'ion-fab is now an ion-fab-button element instead of an Angular directive.', 51 | source 52 | }); 53 | }); 54 | 55 | it('should fail when ion-fab attribute is used on anchor', () => { 56 | let source = ` 57 | @Component({ 58 | template: \` 59 | \` 60 | ~~~~~~~ 61 | }) 62 | class Bar{} 63 | `; 64 | 65 | assertAnnotated({ 66 | ruleName, 67 | message: 'ion-fab is now an ion-fab-button element instead of an Angular directive.', 68 | source 69 | }); 70 | }); 71 | 72 | it('should fail when ion-fab attribute is used with multiline', () => { 73 | let source = ` 74 | @Component({ 75 | template: \` 76 | Click Me\` 80 | }) 81 | class Bar{} 82 | `; 83 | 84 | assertAnnotated({ 85 | ruleName, 86 | message: 'ion-fab is now an ion-fab-button element instead of an Angular directive.', 87 | source 88 | }); 89 | }); 90 | }); 91 | 92 | describe('replacements', () => { 93 | it('should replace button with ion-fab-button and remove attribute', () => { 94 | let source = ` 95 | @Component({ 96 | template: \` 97 | \` 98 | }) 99 | class Bar {} 100 | `; 101 | const fail = { 102 | message: 'ion-fab is now an ion-fab-button element instead of an Angular directive.', 103 | startPosition: { 104 | line: 2, 105 | character: 29 106 | }, 107 | endPosition: { 108 | line: 2, 109 | character: 36 110 | } 111 | }; 112 | 113 | const failures = assertFailure(ruleName, source, fail); 114 | const fixes = failures.map(f => f.getFix()); 115 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 116 | 117 | let expected = ` 118 | @Component({ 119 | template: \` 120 | \` 121 | }) 122 | class Bar {} 123 | `; 124 | expect(res).to.eq(expected); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/ionFabFixedContent.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionFabFixedContentRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail with no attributes', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The ion-fab container is no longer fixed by default. Use slot="fixed".', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail without slot attribute', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'The ion-fab container is no longer fixed by default. Use slot="fixed".', 50 | source 51 | }); 52 | }); 53 | }); 54 | 55 | describe('replacements', () => { 56 | it('should add slot="fixed" to ion-fab without attributes', () => { 57 | let source = ` 58 | @Component({ 59 | template: \` 60 | \` 61 | }) 62 | class Bar {} 63 | `; 64 | 65 | const fail = { 66 | message: 'The ion-fab container is no longer fixed by default. Use slot="fixed".', 67 | startPosition: { 68 | line: 2, 69 | character: 22 70 | }, 71 | endPosition: { 72 | line: 2, 73 | character: 29 74 | } 75 | }; 76 | 77 | const failures = assertFailure(ruleName, source, fail); 78 | const fixes = failures.map(f => f.getFix()); 79 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 80 | 81 | let expected = ` 82 | @Component({ 83 | template: \` 84 | \` 85 | }) 86 | class Bar {} 87 | `; 88 | 89 | expect(res).to.eq(expected); 90 | }); 91 | 92 | it('should add slot="fixed" to ion-radio without slot', () => { 93 | let source = ` 94 | @Component({ 95 | template: \` 96 | 97 | \` 98 | }) 99 | class Bar {} 100 | `; 101 | 102 | const fail = { 103 | message: 'The ion-fab container is no longer fixed by default. Use slot="fixed".', 104 | startPosition: { 105 | line: 3, 106 | character: 13 107 | }, 108 | endPosition: { 109 | line: 3, 110 | character: 20 111 | } 112 | }; 113 | 114 | const failures = assertFailure(ruleName, source, fail); 115 | const fixes = failures.map(f => f.getFix()); 116 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 117 | 118 | let expected = ` 119 | @Component({ 120 | template: \` 121 | 122 | \` 123 | }) 124 | class Bar {} 125 | `; 126 | 127 | expect(res).to.eq(expected); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/ionIconAttributeIsActiveRemoved.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionIconAttributeIsActiveRemovedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail with isActive attribute', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The isActive attribute of ion-icon has been removed.', 33 | source 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /test/ionItemAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionItemAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when item-start is used', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The item-start attribute of ion-item has been renamed. Use slot="start" instead.', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail when item-left is used', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'The item-left attribute of ion-item has been renamed. Use slot="start" instead.', 50 | source 51 | }); 52 | }); 53 | 54 | it('should fail when item-right is used', () => { 55 | let source = ` 56 | @Component({ 57 | template: \` 58 | \` 59 | ~~~~~~~~~~ 60 | }) 61 | class Bar{} 62 | `; 63 | 64 | assertAnnotated({ 65 | ruleName, 66 | message: 'The item-right attribute of ion-item has been renamed. Use slot="end" instead.', 67 | source 68 | }); 69 | }); 70 | 71 | it('should fail when item-end is used', () => { 72 | let source = ` 73 | @Component({ 74 | template: \` 75 | \` 76 | ~~~~~~~~ 77 | }) 78 | class Bar{} 79 | `; 80 | 81 | assertAnnotated({ 82 | ruleName, 83 | message: 'The item-end attribute of ion-item has been renamed. Use slot="end" instead.', 84 | source 85 | }); 86 | }); 87 | }); 88 | 89 | describe('replacements', () => { 90 | it('should replace item-start with slot="start"', () => { 91 | let source = ` 92 | @Component({ 93 | template: \` 94 | \` 95 | }) 96 | class Bar {} 97 | `; 98 | 99 | const fail = { 100 | message: 'The item-start attribute of ion-item has been renamed. Use slot="start" instead.', 101 | startPosition: { 102 | line: 2, 103 | character: 31 104 | }, 105 | endPosition: { 106 | line: 2, 107 | character: 41 108 | } 109 | }; 110 | 111 | const failures = assertFailure(ruleName, source, fail); 112 | const fixes = failures.map(f => f.getFix()); 113 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 114 | 115 | let expected = ` 116 | @Component({ 117 | template: \` 118 | \` 119 | }) 120 | class Bar {} 121 | `; 122 | 123 | expect(res).to.eq(expected); 124 | }); 125 | 126 | it('should replace item-left with slot="start"', () => { 127 | let source = ` 128 | @Component({ 129 | template: \` 130 | \` 131 | }) 132 | class Bar {} 133 | `; 134 | 135 | const fail = { 136 | message: 'The item-left attribute of ion-item has been renamed. Use slot="start" instead.', 137 | startPosition: { 138 | line: 2, 139 | character: 31 140 | }, 141 | endPosition: { 142 | line: 2, 143 | character: 40 144 | } 145 | }; 146 | 147 | const failures = assertFailure(ruleName, source, fail); 148 | const fixes = failures.map(f => f.getFix()); 149 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 150 | 151 | let expected = ` 152 | @Component({ 153 | template: \` 154 | \` 155 | }) 156 | class Bar {} 157 | `; 158 | 159 | expect(res).to.eq(expected); 160 | }); 161 | 162 | it('should replace item-right with slot="end"', () => { 163 | let source = ` 164 | @Component({ 165 | template: \` 166 | \` 167 | }) 168 | class Bar {} 169 | `; 170 | 171 | const fail = { 172 | message: 'The item-right attribute of ion-item has been renamed. Use slot="end" instead.', 173 | startPosition: { 174 | line: 2, 175 | character: 31 176 | }, 177 | endPosition: { 178 | line: 2, 179 | character: 41 180 | } 181 | }; 182 | 183 | const failures = assertFailure(ruleName, source, fail); 184 | const fixes = failures.map(f => f.getFix()); 185 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 186 | 187 | let expected = ` 188 | @Component({ 189 | template: \` 190 | \` 191 | }) 192 | class Bar {} 193 | `; 194 | 195 | expect(res).to.eq(expected); 196 | }); 197 | 198 | it('should replace item-end with slot="end"', () => { 199 | let source = ` 200 | @Component({ 201 | template: \` 202 | \` 203 | }) 204 | class Bar {} 205 | `; 206 | 207 | const fail = { 208 | message: 'The item-end attribute of ion-item has been renamed. Use slot="end" instead.', 209 | startPosition: { 210 | line: 2, 211 | character: 31 212 | }, 213 | endPosition: { 214 | line: 2, 215 | character: 39 216 | } 217 | }; 218 | 219 | const failures = assertFailure(ruleName, source, fail); 220 | const fixes = failures.map(f => f.getFix()); 221 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 222 | 223 | let expected = ` 224 | @Component({ 225 | template: \` 226 | \` 227 | }) 228 | class Bar {} 229 | `; 230 | 231 | expect(res).to.eq(expected); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /test/ionItemDividerIonLabelRequired.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionItemDividerIonLabelRequiredRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with ion-label child', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | 13 | 14 | Dog 15 | 16 | \` 17 | }) 18 | class Bar{} 19 | `; 20 | assertSuccess(ruleName, source); 21 | }); 22 | 23 | it('should work with single ion-label child', () => { 24 | let source = ` 25 | @Component({ 26 | template: \` 27 | 28 | Dog 29 | 30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | assertSuccess(ruleName, source); 35 | }); 36 | }); 37 | 38 | describe('failure', () => { 39 | it('should fail when ion-label missing', () => { 40 | let source = ` 41 | @Component({ 42 | template: \` 43 | 44 | ~~~~~~~~~~~~~~~~ 45 | 46 | 47 | 48 | Dog 49 | 50 | \` 51 | }) 52 | class Bar{} 53 | `; 54 | 55 | assertAnnotated({ 56 | ruleName, 57 | message: 'The ion-item-divider requires an ion-label component. It is no longer automatically added.', 58 | source 59 | }); 60 | }); 61 | 62 | it('should fail with only text', () => { 63 | let source = ` 64 | @Component({ 65 | template: \` 66 | Dog 67 | ~~~~~~~~~~~~~~~~ 68 | \` 69 | }) 70 | class Bar{} 71 | `; 72 | 73 | assertAnnotated({ 74 | ruleName, 75 | message: 'The ion-item-divider requires an ion-label component. It is no longer automatically added.', 76 | source 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/ionItemIonLabelRequired.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionItemIonLabelRequiredRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with ion-label child', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | 13 | 14 | Dog 15 | 16 | \` 17 | }) 18 | class Bar{} 19 | `; 20 | assertSuccess(ruleName, source); 21 | }); 22 | 23 | it('should work with single ion-label child', () => { 24 | let source = ` 25 | @Component({ 26 | template: \` 27 | 28 | Dog 29 | 30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | assertSuccess(ruleName, source); 35 | }); 36 | }); 37 | 38 | describe('failure', () => { 39 | it('should fail when ion-label missing', () => { 40 | let source = ` 41 | @Component({ 42 | template: \` 43 | 44 | ~~~~~~~~ 45 | 46 | 47 | 48 | Dog 49 | 50 | \` 51 | }) 52 | class Bar{} 53 | `; 54 | 55 | assertAnnotated({ 56 | ruleName, 57 | message: 'The ion-item requires an ion-label component. It is no longer automatically added.', 58 | source 59 | }); 60 | }); 61 | 62 | it('should fail with only text', () => { 63 | let source = ` 64 | @Component({ 65 | template: \` 66 | Dog 67 | ~~~~~~~~ 68 | \` 69 | }) 70 | class Bar{} 71 | `; 72 | 73 | assertAnnotated({ 74 | ruleName, 75 | message: 'The ion-item requires an ion-label component. It is no longer automatically added.', 76 | source 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/ionItemIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName } from '../src/ionItemIsNowAnElementRule'; 3 | import { assertAnnotated, assertSuccess, assertFailure } from './testHelper'; 4 | import { Utils, Replacement } from 'tslint'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when ion-item attribute is used on button', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'ion-item is now an ion-item element instead of an Angular directive.', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail when ion-item attribute is used on anchor', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'ion-item is now an ion-item element instead of an Angular directive.', 50 | source 51 | }); 52 | }); 53 | 54 | it('should fail when ion-item attribute is used with multiline', () => { 55 | let source = ` 56 | @Component({ 57 | template: \` 58 | Click Me\` 62 | }) 63 | class Bar{} 64 | `; 65 | 66 | assertAnnotated({ 67 | ruleName, 68 | message: 'ion-item is now an ion-item element instead of an Angular directive.', 69 | source 70 | }); 71 | }); 72 | }); 73 | 74 | describe('replacements', () => { 75 | it('should replace button with ion-item and remove attribute', () => { 76 | let source = ` 77 | @Component({ 78 | template: \` 79 | \` 80 | }) 81 | class Bar {} 82 | `; 83 | const fail = { 84 | message: 'ion-item is now an ion-item element instead of an Angular directive.', 85 | startPosition: { 86 | line: 2, 87 | character: 29 88 | }, 89 | endPosition: { 90 | line: 2, 91 | character: 37 92 | } 93 | }; 94 | 95 | const failures = assertFailure(ruleName, source, fail); 96 | const fixes = failures.map(f => f.getFix()); 97 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 98 | 99 | let expected = ` 100 | @Component({ 101 | template: \` 102 | \` 103 | }) 104 | class Bar {} 105 | `; 106 | expect(res).to.eq(expected); 107 | }); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /test/ionItemOptionIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionItemOptionIsNowAnElementRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with proper style', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | Item 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | \` 21 | }) 22 | class Bar{} 23 | `; 24 | assertSuccess(ruleName, source); 25 | }); 26 | }); 27 | 28 | describe('failure', () => { 29 | it('should fail when ion-button attribute is used on a button within ion-item-options', () => { 30 | let source = ` 31 | @Component({ 32 | template: \` 33 | 34 | 38 | 39 | \` 40 | }) 41 | class Bar{} 42 | `; 43 | 44 | assertAnnotated({ 45 | ruleName, 46 | message: 'Buttons within ion-item-options are now ion-item-option elements instead of Angular directives.', 47 | source 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/ionItemOptionMarkupHasChanged.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName, ruleMessage as message } from '../src/ionItemOptionMarkupHasChangedRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work when ion-item-option is used', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | Item 1 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | \` 21 | }) 22 | class Bar{} 23 | `; 24 | assertSuccess(ruleName, source); 25 | }); 26 | }); 27 | 28 | describe('failure', () => { 29 | it('should fail when ion-button is used', () => { 30 | let source = ` 31 | @Component({ 32 | template: \` 33 | 34 | 35 | Item 1 36 | 37 | 38 | 42 | 43 | 44 | \` 45 | }) 46 | class Bar{} 47 | `; 48 | 49 | assertAnnotated({ 50 | ruleName, 51 | message, 52 | source 53 | }); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/ionItemOptionMethodGetSlidingPercentRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName, ruleMessage } from '../src/ionItemOptionMethodGetSlidingPercentRenamedRule'; 3 | import { assertAnnotated, assertSuccess } from './testHelper'; 4 | 5 | describe(ruleName, () => { 6 | describe('success', () => { 7 | it('should work with new method name', () => { 8 | let source = ` 9 | class DoSomething{ 10 | constructor(){} 11 | getRatio(item: ItemSliding){ 12 | return item.getSlidingRatio(); 13 | } 14 | } 15 | `; 16 | assertSuccess(ruleName, source); 17 | }); 18 | }); 19 | 20 | describe('failure', () => { 21 | it('should fail when using getSlidingPercent', () => { 22 | let source = ` 23 | class DoSomething{ 24 | constructor(){} 25 | getRatio(item: ItemSliding){ 26 | return item.getSlidingPercent(); 27 | ~~~~~~~~~~~~~~~~~ 28 | } 29 | } 30 | `; 31 | assertAnnotated({ 32 | ruleName, 33 | message: ruleMessage, 34 | source 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/ionItemOptionsAttributeValuesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionItemOptionsAttributeValuesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | 14 | Item 1 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | \` 23 | }) 24 | class Bar{} 25 | `; 26 | assertSuccess(ruleName, source); 27 | }); 28 | }); 29 | 30 | describe('failure', () => { 31 | it('should fail when side="left" is used', () => { 32 | let source = ` 33 | @Component({ 34 | template: \` 35 | 36 | 37 | Item 1 38 | 39 | 40 | ~~~~~~~~~~~ 41 | 42 | 43 | 44 | 45 | 46 | \` 47 | }) 48 | class Bar{} 49 | `; 50 | 51 | assertAnnotated({ 52 | ruleName, 53 | message: 'The side="left" attribute/value of ion-item-options should be written as side="start".', 54 | source 55 | }); 56 | }); 57 | 58 | it('should fail when side="right" is used', () => { 59 | let source = ` 60 | @Component({ 61 | template: \` 62 | 63 | 64 | Item 1 65 | 66 | 67 | ~~~~~~~~~~~~ 68 | 69 | 70 | 71 | 72 | 73 | \` 74 | }) 75 | class Bar{} 76 | `; 77 | 78 | assertAnnotated({ 79 | ruleName, 80 | message: 'The side="right" attribute/value of ion-item-options should be written as side="end".', 81 | source 82 | }); 83 | }); 84 | }); 85 | 86 | describe('replacements', () => { 87 | it('should replace side="left" with side="start"', () => { 88 | let source = ` 89 | @Component({ 90 | template: \` 91 | 92 | 93 | Item 1 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | \` 102 | }) 103 | class Bar {} 104 | `; 105 | 106 | const fail = { 107 | message: 'The side="left" attribute/value of ion-item-options should be written as side="start".', 108 | startPosition: { 109 | line: 7, 110 | character: 32 111 | }, 112 | endPosition: { 113 | line: 7, 114 | character: 43 115 | } 116 | }; 117 | 118 | const failures = assertFailure(ruleName, source, fail); 119 | const fixes = failures.map(f => f.getFix()); 120 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 121 | 122 | let expected = ` 123 | @Component({ 124 | template: \` 125 | 126 | 127 | Item 1 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | \` 136 | }) 137 | class Bar {} 138 | `; 139 | 140 | expect(res).to.eq(expected); 141 | }); 142 | 143 | it('should replace side="right" with side="end"', () => { 144 | let source = ` 145 | @Component({ 146 | template: \` 147 | 148 | 149 | Item 1 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | \` 158 | }) 159 | class Bar {} 160 | `; 161 | 162 | const fail = { 163 | message: 'The side="right" attribute/value of ion-item-options should be written as side="end".', 164 | startPosition: { 165 | line: 7, 166 | character: 32 167 | }, 168 | endPosition: { 169 | line: 7, 170 | character: 44 171 | } 172 | }; 173 | 174 | const failures = assertFailure(ruleName, source, fail); 175 | const fixes = failures.map(f => f.getFix()); 176 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 177 | 178 | let expected = ` 179 | @Component({ 180 | template: \` 181 | 182 | 183 | Item 1 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | \` 192 | }) 193 | class Bar {} 194 | `; 195 | 196 | expect(res).to.eq(expected); 197 | }); 198 | }); 199 | }); 200 | -------------------------------------------------------------------------------- /test/ionLabelAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionLabelAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when fixed is used', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The fixed attribute of ion-label has been renamed. Use position="fixed" instead.', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail when floating is used', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'The floating attribute of ion-label has been renamed. Use position="floating" instead.', 50 | source 51 | }); 52 | }); 53 | 54 | it('should fail when stacked is used', () => { 55 | let source = ` 56 | @Component({ 57 | template: \` 58 | \` 59 | ~~~~~~~ 60 | }) 61 | class Bar{} 62 | `; 63 | 64 | assertAnnotated({ 65 | ruleName, 66 | message: 'The stacked attribute of ion-label has been renamed. Use position="stacked" instead.', 67 | source 68 | }); 69 | }); 70 | }); 71 | 72 | describe('replacements', () => { 73 | it('should replace fixed with position="fixed"', () => { 74 | let source = ` 75 | @Component({ 76 | template: \` 77 | \` 78 | }) 79 | class Bar {} 80 | `; 81 | 82 | const fail = { 83 | message: 'The fixed attribute of ion-label has been renamed. Use position="fixed" instead.', 84 | startPosition: { 85 | line: 2, 86 | character: 32 87 | }, 88 | endPosition: { 89 | line: 2, 90 | character: 37 91 | } 92 | }; 93 | 94 | const failures = assertFailure(ruleName, source, fail); 95 | const fixes = failures.map(f => f.getFix()); 96 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 97 | 98 | let expected = ` 99 | @Component({ 100 | template: \` 101 | \` 102 | }) 103 | class Bar {} 104 | `; 105 | 106 | expect(res).to.eq(expected); 107 | }); 108 | 109 | it('should replace floating with position="floating"', () => { 110 | let source = ` 111 | @Component({ 112 | template: \` 113 | \` 114 | }) 115 | class Bar {} 116 | `; 117 | 118 | const fail = { 119 | message: 'The floating attribute of ion-label has been renamed. Use position="floating" instead.', 120 | startPosition: { 121 | line: 2, 122 | character: 32 123 | }, 124 | endPosition: { 125 | line: 2, 126 | character: 40 127 | } 128 | }; 129 | 130 | const failures = assertFailure(ruleName, source, fail); 131 | const fixes = failures.map(f => f.getFix()); 132 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 133 | 134 | let expected = ` 135 | @Component({ 136 | template: \` 137 | \` 138 | }) 139 | class Bar {} 140 | `; 141 | 142 | expect(res).to.eq(expected); 143 | }); 144 | 145 | it('should replace stacked with position="stacked"', () => { 146 | let source = ` 147 | @Component({ 148 | template: \` 149 | \` 150 | }) 151 | class Bar {} 152 | `; 153 | 154 | const fail = { 155 | message: 'The stacked attribute of ion-label has been renamed. Use position="stacked" instead.', 156 | startPosition: { 157 | line: 2, 158 | character: 32 159 | }, 160 | endPosition: { 161 | line: 2, 162 | character: 39 163 | } 164 | }; 165 | 166 | const failures = assertFailure(ruleName, source, fail); 167 | const fixes = failures.map(f => f.getFix()); 168 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 169 | 170 | let expected = ` 171 | @Component({ 172 | template: \` 173 | \` 174 | }) 175 | class Bar {} 176 | `; 177 | 178 | expect(res).to.eq(expected); 179 | }); 180 | }); 181 | }); 182 | -------------------------------------------------------------------------------- /test/ionListHeaderIonLabelRequired.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionListHeaderIonLabelRequiredRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with ion-label child', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | 13 | 14 | Dog 15 | 16 | \` 17 | }) 18 | class Bar{} 19 | `; 20 | assertSuccess(ruleName, source); 21 | }); 22 | 23 | it('should work with single ion-label child', () => { 24 | let source = ` 25 | @Component({ 26 | template: \` 27 | 28 | Dog 29 | 30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | assertSuccess(ruleName, source); 35 | }); 36 | }); 37 | 38 | describe('failure', () => { 39 | it('should fail when ion-label missing', () => { 40 | let source = ` 41 | @Component({ 42 | template: \` 43 | 44 | ~~~~~~~~~~~~~~~ 45 | 46 | 47 | 48 | Dog 49 | 50 | \` 51 | }) 52 | class Bar{} 53 | `; 54 | 55 | assertAnnotated({ 56 | ruleName, 57 | message: 'The ion-list-header requires an ion-label component. It is no longer automatically added.', 58 | source 59 | }); 60 | }); 61 | 62 | it('should fail with only text', () => { 63 | let source = ` 64 | @Component({ 65 | template: \` 66 | Dog 67 | ~~~~~~~~~~~~~~~ 68 | \` 69 | }) 70 | class Bar{} 71 | `; 72 | 73 | assertAnnotated({ 74 | ruleName, 75 | message: 'The ion-list-header requires an ion-label component. It is no longer automatically added.', 76 | source 77 | }); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /test/ionLoadingMethodCreateParametersRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionLoadingMethodCreateParametersRenamedRule'; 4 | import { assertAnnotated, assertFailures, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | class DoSomething{ 11 | constructor(private loadingCtrl: LoadingController){} 12 | 13 | showLoading(){ 14 | const loading = await this.loadingCtrl.create({ 15 | message: 'This is the title' 16 | }); 17 | await loading.present(); 18 | } 19 | } 20 | `; 21 | assertSuccess(ruleName, source); 22 | }); 23 | 24 | it('should not be triggered if the type is not LoadingController', () => { 25 | let source = ` 26 | class DoSomething{ 27 | constructor(private loadingCtrl: SomeOtherController){} 28 | 29 | showLoading(){ 30 | const loading = await this.loadingCtrl.create({ 31 | content: 'This is the title' 32 | }); 33 | await loading.present(); 34 | } 35 | } 36 | `; 37 | assertSuccess(ruleName, source); 38 | }); 39 | 40 | it('should work with different names for the LoadingController object', () => { 41 | let source = ` 42 | class DoSomething{ 43 | constructor(private myOtherNamedCtrl: LoadingController){} 44 | 45 | showLoading(){ 46 | const loading = await this.myOtherNamedCtrl.create({ 47 | message: 'This is the title' 48 | }); 49 | await loading.present(); 50 | } 51 | } 52 | `; 53 | assertSuccess(ruleName, source); 54 | }); 55 | }); 56 | 57 | describe('failure', () => { 58 | it('should fail when content is passed in', () => { 59 | let source = ` 60 | class DoSomething{ 61 | constructor(private loadingCtrl: LoadingController){} 62 | 63 | showLoading(){ 64 | const loading = await this.loadingCtrl.create({ 65 | content: 'This is the title' 66 | ~~~~~~~ 67 | }); 68 | await loading.present(); 69 | } 70 | } 71 | `; 72 | 73 | assertAnnotated({ 74 | ruleName, 75 | message: 'Property content has been renamed to message.', 76 | source 77 | }); 78 | }); 79 | }); 80 | 81 | describe('replacements', () => { 82 | it('should replace content', () => { 83 | let source = ` 84 | class DoSomething{ 85 | constructor(private loadingCtrl: LoadingController){} 86 | 87 | showLoading(){ 88 | const loading = await this.loadingCtrl.create({ 89 | content: 'This is the title' 90 | }); 91 | await loading.present(); 92 | } 93 | } 94 | `; 95 | 96 | const failures = assertFailures(ruleName, source, [ 97 | { 98 | message: 'Property content has been renamed to message.', 99 | startPosition: { 100 | line: 6, 101 | character: 12 102 | }, 103 | endPosition: { 104 | line: 6, 105 | character: 19 106 | } 107 | } 108 | ]); 109 | 110 | const fixes = failures.map(f => f.getFix()); 111 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 112 | 113 | expect(res).to.eq(` 114 | class DoSomething{ 115 | constructor(private loadingCtrl: LoadingController){} 116 | 117 | showLoading(){ 118 | const loading = await this.loadingCtrl.create({ 119 | message: 'This is the title' 120 | }); 121 | await loading.present(); 122 | } 123 | } 124 | `); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/ionMenuEventsRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionMenuEventsRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('events should be ionDidClose and ionDidOpen', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | 14 | \` 15 | }) 16 | class Bar{} 17 | `; 18 | assertSuccess(ruleName, source); 19 | }); 20 | }); 21 | 22 | describe('failure', () => { 23 | it('should fail when ionOpen is used', () => { 24 | let source = ` 25 | @Component({ 26 | template: \` 27 | 28 | ~~~~~~~ 29 | 30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | 35 | assertAnnotated({ 36 | ruleName, 37 | message: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.', 38 | source 39 | }); 40 | }); 41 | 42 | it('should fail when ionClose is used', () => { 43 | let source = ` 44 | @Component({ 45 | template: \` 46 | 47 | ~~~~~~~~ 48 | 49 | \` 50 | }) 51 | class Bar{} 52 | `; 53 | 54 | assertAnnotated({ 55 | ruleName, 56 | message: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.', 57 | source 58 | }); 59 | }); 60 | 61 | it('should fail when ionClose and ionOpen are used', () => { 62 | let source = ` 63 | @Component({ 64 | template: \` 65 | 66 | ~~~~~~~~ ^^^^^^^ 67 | 68 | \` 69 | }) 70 | class Bar{} 71 | `; 72 | 73 | assertMultipleAnnotated({ 74 | ruleName, 75 | source, 76 | failures: [ 77 | { 78 | char: '~', 79 | msg: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.' 80 | }, 81 | { 82 | char: '^', 83 | msg: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.' 84 | } 85 | ] 86 | }); 87 | }); 88 | }); 89 | // 90 | describe('replacements', () => { 91 | it('should replace ionOpen with ionDidOpen', () => { 92 | let source = ` 93 | @Component({ 94 | template: \` 95 | 96 | 97 | \` 98 | }) 99 | class Bar {} 100 | `; 101 | 102 | const fail = { 103 | message: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.', 104 | startPosition: { 105 | line: 3, 106 | character: 36 107 | }, 108 | endPosition: { 109 | line: 3, 110 | character: 43 111 | } 112 | }; 113 | 114 | const failures = assertFailure(ruleName, source, fail); 115 | const fixes = failures.map(f => f.getFix()); 116 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 117 | 118 | let expected = ` 119 | @Component({ 120 | template: \` 121 | 122 | 123 | \` 124 | }) 125 | class Bar {} 126 | `; 127 | 128 | expect(res).to.eq(expected); 129 | }); 130 | 131 | it('should replace ionClose with ionDidClose', () => { 132 | let source = ` 133 | @Component({ 134 | template: \` 135 | 136 | 137 | \` 138 | }) 139 | class Bar {} 140 | `; 141 | 142 | const fail = { 143 | message: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.', 144 | startPosition: { 145 | line: 3, 146 | character: 36 147 | }, 148 | endPosition: { 149 | line: 3, 150 | character: 44 151 | } 152 | }; 153 | 154 | const failures = assertFailure(ruleName, source, fail); 155 | const fixes = failures.map(f => f.getFix()); 156 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 157 | 158 | let expected = ` 159 | @Component({ 160 | template: \` 161 | 162 | 163 | \` 164 | }) 165 | class Bar {} 166 | `; 167 | 168 | expect(res).to.eq(expected); 169 | }); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /test/ionMenuToggleIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionMenuToggleIsNowAnElementRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with proper style', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | 12 | Toggle Menu 13 | 14 | 15 | \` 16 | }) 17 | class Bar{} 18 | `; 19 | assertSuccess(ruleName, source); 20 | }); 21 | }); 22 | 23 | describe('failure', () => { 24 | it('should fail when menuToggle attribute is used on button', () => { 25 | let source = ` 26 | @Component({ 27 | template: \` 28 | \` 29 | ~~~~~~~~~~ 30 | }) 31 | class Bar{} 32 | `; 33 | 34 | assertAnnotated({ 35 | ruleName, 36 | message: 'menuToggle is now an ion-menu-toggle element instead of an Angular directive.', 37 | source 38 | }); 39 | }); 40 | 41 | it('should fail when menuToggle attribute is used on anchor', () => { 42 | let source = ` 43 | @Component({ 44 | template: \` 45 | Toggle Menu\` 46 | ~~~~~~~~~~ 47 | }) 48 | class Bar{} 49 | `; 50 | 51 | assertAnnotated({ 52 | ruleName, 53 | message: 'menuToggle is now an ion-menu-toggle element instead of an Angular directive.', 54 | source 55 | }); 56 | }); 57 | 58 | it('should fail when ion-button attribute is used with multiline', () => { 59 | let source = ` 60 | @Component({ 61 | template: \` 62 | Toggle Menu\` 67 | }) 68 | class Bar{} 69 | `; 70 | 71 | assertAnnotated({ 72 | ruleName, 73 | message: 'menuToggle is now an ion-menu-toggle element instead of an Angular directive.', 74 | source 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/ionNavbarIsNowIonToolbarRule.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertSuccess, assertAnnotated, assertFailure } from './testHelper'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { expect } from 'chai'; 4 | import { ruleName } from '../src/ionNavbarIsNowIonToolbarRule'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` \` 12 | }) 13 | class Bar {} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when navbar is passed in', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | 25 | ~~~~~~~~~~ 26 | \` 27 | }) 28 | class Bar {} 29 | `; 30 | 31 | assertAnnotated({ 32 | ruleName, 33 | source, 34 | message: 'The ion-navbar component is now named ion-toolbar.' 35 | }); 36 | }); 37 | 38 | it('should fail when navbar is passed in', () => { 39 | let source = ` 40 | @Component({ 41 | template: \` 42 | 43 | ~~~~~~~~~~ 44 | 45 | 46 | \` 47 | }) 48 | class Bar {} 49 | `; 50 | 51 | assertAnnotated({ 52 | ruleName, 53 | source, 54 | message: 'The ion-navbar component is now named ion-toolbar.' 55 | }); 56 | }); 57 | }); 58 | 59 | describe('replacements', () => { 60 | it('should fail when ion-navbar is used', () => { 61 | let source = ` 62 | @Component({ 63 | template: \` 64 | 65 | \` 66 | }) 67 | class Bar {} 68 | `; 69 | 70 | const fail = { 71 | message: 'The ion-navbar component is now named ion-toolbar.', 72 | startPosition: { 73 | line: 3, 74 | character: 13 75 | }, 76 | endPosition: { 77 | line: 3, 78 | character: 23 79 | } 80 | }; 81 | 82 | const failures = assertFailure(ruleName, source, fail); 83 | const fixes = failures.map(f => f.getFix()); 84 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 85 | 86 | expect(res).to.eq(` 87 | @Component({ 88 | template: \` 89 | 90 | \` 91 | }) 92 | class Bar {} 93 | `); 94 | }); 95 | 96 | it('should fail when ion-navbar is used on multiple lines', () => { 97 | let source = ` 98 | @Component({ 99 | template: \` 100 | 101 | My Navigation Bar 102 | 103 | \` 104 | }) 105 | class Bar {} 106 | `; 107 | 108 | const fail = { 109 | message: 'The ion-navbar component is now named ion-toolbar.', 110 | startPosition: { 111 | line: 3, 112 | character: 13 113 | }, 114 | endPosition: { 115 | line: 3, 116 | character: 23 117 | } 118 | }; 119 | 120 | const failures = assertFailure(ruleName, source, fail); 121 | const fixes = failures.map(f => f.getFix()); 122 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 123 | 124 | expect(res).to.eq(` 125 | @Component({ 126 | template: \` 127 | 128 | My Navigation Bar 129 | 130 | \` 131 | }) 132 | class Bar {} 133 | `); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /test/ionOptionIsNowIonSelectOption.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertSuccess, assertAnnotated, assertMultipleAnnotated, assertFailures } from './testHelper'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { expect } from 'chai'; 4 | import { ruleName } from '../src/ionOptionIsNowIonSelectOptionRule'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | Option 1 14 | Option 2 15 | Option 3 16 | 17 | \` 18 | }) 19 | class Bar {} 20 | `; 21 | assertSuccess(ruleName, source); 22 | }); 23 | }); 24 | 25 | describe('failure', () => { 26 | it('should fail when ion-option is used', () => { 27 | let source = ` 28 | @Component({ 29 | template: \` 30 | 31 | Option 1 32 | ~~~~~~~~~~ 33 | Option 2 34 | ^^^^^^^^^^ 35 | Option 3 36 | zzzzzzzzzz 37 | 38 | \` 39 | }) 40 | class Bar {} 41 | `; 42 | 43 | assertMultipleAnnotated({ 44 | ruleName, 45 | source, 46 | failures: [ 47 | { 48 | char: '~', 49 | msg: 'The ion-option component is now named ion-select-option.' 50 | }, 51 | { 52 | char: '^', 53 | msg: 'The ion-option component is now named ion-select-option.' 54 | }, 55 | { 56 | char: 'z', 57 | msg: 'The ion-option component is now named ion-select-option.' 58 | } 59 | ] 60 | }); 61 | }); 62 | }); 63 | 64 | describe('replacements', () => { 65 | it('should replace ion-option with ion-select-option', () => { 66 | let source = ` 67 | @Component({ 68 | template: \` 69 | 70 | Option 1 71 | Option 2 72 | Option 3 73 | 74 | \` 75 | }) 76 | class Bar {} 77 | `; 78 | 79 | const failures = assertFailures(ruleName, source, [ 80 | { 81 | message: 'The ion-option component is now named ion-select-option.', 82 | startPosition: { 83 | line: 4, 84 | character: 15 85 | }, 86 | endPosition: { 87 | line: 4, 88 | character: 25 89 | } 90 | }, 91 | { 92 | message: 'The ion-option component is now named ion-select-option.', 93 | startPosition: { 94 | line: 5, 95 | character: 15 96 | }, 97 | endPosition: { 98 | line: 5, 99 | character: 25 100 | } 101 | }, 102 | { 103 | message: 'The ion-option component is now named ion-select-option.', 104 | startPosition: { 105 | line: 6, 106 | character: 15 107 | }, 108 | endPosition: { 109 | line: 6, 110 | character: 25 111 | } 112 | } 113 | ]); 114 | 115 | const fixes = failures.map(f => f.getFix()); 116 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 117 | 118 | let expected = ` 119 | @Component({ 120 | template: \` 121 | 122 | Option 1 123 | Option 2 124 | Option 3 125 | 126 | \` 127 | }) 128 | class Bar {} 129 | `; 130 | 131 | expect(res).to.eq(expected); 132 | }); 133 | 134 | it('should replace ion-option with ion-select-option on multiline', () => { 135 | let source = ` 136 | @Component({ 137 | template: \` 138 | 139 | 140 | Female 141 | 142 | 143 | Male 144 | 145 | 146 | \` 147 | }) 148 | class Bar {} 149 | `; 150 | 151 | const failures = assertFailures(ruleName, source, [ 152 | { 153 | message: 'The ion-option component is now named ion-select-option.', 154 | startPosition: { 155 | line: 4, 156 | character: 15 157 | }, 158 | endPosition: { 159 | line: 4, 160 | character: 25 161 | } 162 | }, 163 | { 164 | message: 'The ion-option component is now named ion-select-option.', 165 | startPosition: { 166 | line: 7, 167 | character: 15 168 | }, 169 | endPosition: { 170 | line: 7, 171 | character: 25 172 | } 173 | } 174 | ]); 175 | 176 | const fixes = failures.map(f => f.getFix()); 177 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 178 | 179 | let expected = ` 180 | @Component({ 181 | template: \` 182 | 183 | 184 | Female 185 | 186 | 187 | Male 188 | 189 | 190 | \` 191 | }) 192 | class Bar {} 193 | `; 194 | 195 | expect(res).to.eq(expected); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /test/ionOverlayMethodCreateShouldUseAwaitRule.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleMessage, ruleName } from '../src/ionOverlayMethodCreateShouldUseAwaitRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work when await exists in front of the create method', () => { 7 | let source = ` 8 | class DoSomething{ 9 | constructor(private actionSheetController: ActionSheetController){} 10 | 11 | doWork(){ 12 | await this.actionSheetController.create({ 13 | component: PopoverComponent, 14 | ev: event, 15 | translucent: true 16 | }) 17 | } 18 | } 19 | `; 20 | assertSuccess(ruleName, source); 21 | }); 22 | 23 | it('should work when then() is used', () => { 24 | let source = ` 25 | class DoSomething{ 26 | constructor(private actionSheetController: ActionSheetController){} 27 | 28 | doWork(){ 29 | this.actionSheetController.create({ 30 | component: PopoverComponent, 31 | ev: event, 32 | translucent: true 33 | }).then(() => {}); 34 | } 35 | } 36 | `; 37 | assertSuccess(ruleName, source); 38 | }); 39 | 40 | it('should work when the controller variable is named something else', () => { 41 | let source = ` 42 | class DoSomething{ 43 | constructor(private strangelyNamedCtrl: ActionSheetController){} 44 | 45 | doWork(){ 46 | await this.strangelyNamedCtrl.create({ 47 | component: PopoverComponent, 48 | ev: event, 49 | translucent: true 50 | }) 51 | } 52 | } 53 | `; 54 | assertSuccess(ruleName, source); 55 | }); 56 | 57 | it('should not do anything if the class name does not match', () => { 58 | let source = ` 59 | class DoSomething{ 60 | constructor(private someOtherController: SomeOtherController){} 61 | 62 | doWork(){ 63 | await this.someOtherController.create({ 64 | component: PopoverComponent, 65 | ev: event, 66 | translucent: true 67 | }) 68 | } 69 | } 70 | `; 71 | assertSuccess(ruleName, source); 72 | }); 73 | }); 74 | 75 | const controllersList = [ 76 | 'ActionSheetController', 77 | 'AlertController', 78 | 'LoadingController', 79 | 'ModalController', 80 | 'PopoverController', 81 | 'ToastController' 82 | ]; 83 | 84 | for (let controller of controllersList) { 85 | describe(controller, () => { 86 | describe('failure', () => { 87 | it('should fail if await is not present', () => { 88 | let source = ` 89 | class DoSomething{ 90 | constructor(private poorlyNamedController: ${controller}){} 91 | 92 | doWork(){ 93 | this.poorlyNamedController.create({ 94 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 95 | component: PopoverComponent, 96 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 97 | ev: event, 98 | ~~~~~~~~~~ 99 | translucent: true 100 | ~~~~~~~~~~~~~~~~~ 101 | }) 102 | ~~ 103 | } 104 | } 105 | `; 106 | assertAnnotated({ 107 | ruleName, 108 | message: ruleMessage, 109 | source 110 | }); 111 | }); 112 | }); 113 | }); 114 | } 115 | }); 116 | -------------------------------------------------------------------------------- /test/ionOverlayMethodPresentShouldUseAwaitRule.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleMessage, ruleName } from '../src/ionOverlayMethodPresentShouldUseAwaitRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work when await exists in front of the present method', () => { 7 | let source = ` 8 | class DoSomething{ 9 | constructor(private actionSheetController: ActionSheetController){} 10 | 11 | doWork(){ 12 | var ctrl = await this.actionSheetController.create({ 13 | component: PopoverComponent, 14 | ev: event, 15 | translucent: true 16 | }); 17 | 18 | await ctrl.present(); 19 | } 20 | } 21 | `; 22 | assertSuccess(ruleName, source); 23 | }); 24 | 25 | it('should work when then() is used', () => { 26 | let source = ` 27 | class DoSomething{ 28 | constructor(private actionSheetController: ActionSheetController){} 29 | 30 | doWork(){ 31 | this.actionSheetController.create({ 32 | component: PopoverComponent, 33 | ev: event, 34 | translucent: true 35 | }).then(() => {}); 36 | } 37 | } 38 | `; 39 | assertSuccess(ruleName, source); 40 | }); 41 | }); 42 | 43 | describe('failure', () => { 44 | it('should fail if await is not present', () => { 45 | let source = ` 46 | class DoSomething{ 47 | constructor(private actionSheetController: ActionSheetController){} 48 | 49 | doWork(){ 50 | var ctrl = this.actionSheetController.create({ 51 | component: PopoverComponent, 52 | ev: event, 53 | translucent: true 54 | }); 55 | 56 | ctrl.present(); 57 | ~~~~~~~~~~~~~~ 58 | } 59 | } 60 | `; 61 | assertAnnotated({ 62 | ruleName, 63 | message: ruleMessage, 64 | source 65 | }); 66 | }); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /test/ionRadioAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionRadioAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail when item-left is used', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The item-left attribute of ion-radio has been renamed. Use slot="start" instead.', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail when item-right is used', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'The item-right attribute of ion-radio has been renamed. Use slot="end" instead.', 50 | source 51 | }); 52 | }); 53 | }); 54 | 55 | describe('replacements', () => { 56 | it('should replace item-left with slot="start"', () => { 57 | let source = ` 58 | @Component({ 59 | template: \` 60 | \` 61 | }) 62 | class Bar {} 63 | `; 64 | 65 | const fail = { 66 | message: 'The item-left attribute of ion-radio has been renamed. Use slot="start" instead.', 67 | startPosition: { 68 | line: 2, 69 | character: 32 70 | }, 71 | endPosition: { 72 | line: 2, 73 | character: 41 74 | } 75 | }; 76 | 77 | const failures = assertFailure(ruleName, source, fail); 78 | const fixes = failures.map(f => f.getFix()); 79 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 80 | 81 | let expected = ` 82 | @Component({ 83 | template: \` 84 | \` 85 | }) 86 | class Bar {} 87 | `; 88 | 89 | expect(res).to.eq(expected); 90 | }); 91 | 92 | it('should replace item-right with slot="end"', () => { 93 | let source = ` 94 | @Component({ 95 | template: \` 96 | \` 97 | }) 98 | class Bar {} 99 | `; 100 | 101 | const fail = { 102 | message: 'The item-right attribute of ion-radio has been renamed. Use slot="end" instead.', 103 | startPosition: { 104 | line: 2, 105 | character: 32 106 | }, 107 | endPosition: { 108 | line: 2, 109 | character: 42 110 | } 111 | }; 112 | 113 | const failures = assertFailure(ruleName, source, fail); 114 | const fixes = failures.map(f => f.getFix()); 115 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 116 | 117 | let expected = ` 118 | @Component({ 119 | template: \` 120 | \` 121 | }) 122 | class Bar {} 123 | `; 124 | 125 | expect(res).to.eq(expected); 126 | }); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /test/ionRadioGroupIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName } from '../src/ionRadioGroupIsNowAnElementRule'; 3 | import { assertAnnotated, assertSuccess, assertFailure } from './testHelper'; 4 | import { Replacement, Utils } from 'tslint'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | 14 | 15 | Apple 16 | 17 | 18 | 19 | 20 | \` 21 | }) 22 | class Bar{} 23 | `; 24 | assertSuccess(ruleName, source); 25 | }); 26 | }); 27 | 28 | describe('failure', () => { 29 | it('should fail when radio-group attribute is used on ion-list', () => { 30 | let source = ` 31 | @Component({ 32 | template: \` 33 | 34 | ~~~~~~~~~~~ 35 | 36 | Apple 37 | 38 | 39 | 40 | \` 41 | }) 42 | class Bar{} 43 | `; 44 | 45 | assertAnnotated({ 46 | ruleName, 47 | message: 'radio-group is now an ion-radio-group element instead of an Angular directive.', 48 | source 49 | }); 50 | }); 51 | }); 52 | 53 | describe('replacements', () => { 54 | it('should create child ion-radio-group element and remove radio-group attribute', () => { 55 | let source = ` 56 | @Component({ 57 | template: \` 58 | \` 59 | }) 60 | class Bar {} 61 | `; 62 | const fail = { 63 | message: 'radio-group is now an ion-radio-group element instead of an Angular directive.', 64 | startPosition: { 65 | line: 2, 66 | character: 31 67 | }, 68 | endPosition: { 69 | line: 2, 70 | character: 42 71 | } 72 | }; 73 | 74 | const failures = assertFailure(ruleName, source, fail); 75 | const fixes = failures.map(f => f.getFix()); 76 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 77 | 78 | let expected = ` 79 | @Component({ 80 | template: \` 81 | 82 | 83 | \` 84 | }) 85 | class Bar {} 86 | `; 87 | expect(res).to.eq(expected); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /test/ionRadioSlotRequired.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionRadioSlotRequiredRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | }); 18 | 19 | describe('failure', () => { 20 | it('should fail with no attributes', () => { 21 | let source = ` 22 | @Component({ 23 | template: \` 24 | \` 25 | ~~~~~~~~~ 26 | }) 27 | class Bar{} 28 | `; 29 | 30 | assertAnnotated({ 31 | ruleName, 32 | message: 'The slot attribute of ion-radio is required. Use slot="start".', 33 | source 34 | }); 35 | }); 36 | 37 | it('should fail without slot attribute', () => { 38 | let source = ` 39 | @Component({ 40 | template: \` 41 | \` 42 | ~~~~~~~~~ 43 | }) 44 | class Bar{} 45 | `; 46 | 47 | assertAnnotated({ 48 | ruleName, 49 | message: 'The slot attribute of ion-radio is required. Use slot="start".', 50 | source 51 | }); 52 | }); 53 | }); 54 | 55 | describe('replacements', () => { 56 | it('should add slot="start" to ion-radio without attributes', () => { 57 | let source = ` 58 | @Component({ 59 | template: \` 60 | \` 61 | }) 62 | class Bar {} 63 | `; 64 | 65 | const fail = { 66 | message: 'The slot attribute of ion-radio is required. Use slot="start".', 67 | startPosition: { 68 | line: 2, 69 | character: 22 70 | }, 71 | endPosition: { 72 | line: 2, 73 | character: 31 74 | } 75 | }; 76 | 77 | const failures = assertFailure(ruleName, source, fail); 78 | const fixes = failures.map(f => f.getFix()); 79 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 80 | 81 | let expected = ` 82 | @Component({ 83 | template: \` 84 | \` 85 | }) 86 | class Bar {} 87 | `; 88 | 89 | expect(res).to.eq(expected); 90 | }); 91 | 92 | it('should add slot="start" to ion-radio without slot', () => { 93 | let source = ` 94 | @Component({ 95 | template: \` 96 | 97 | \` 98 | }) 99 | class Bar {} 100 | `; 101 | 102 | const fail = { 103 | message: 'The slot attribute of ion-radio is required. Use slot="start".', 104 | startPosition: { 105 | line: 3, 106 | character: 13 107 | }, 108 | endPosition: { 109 | line: 3, 110 | character: 22 111 | } 112 | }; 113 | 114 | const failures = assertFailure(ruleName, source, fail); 115 | const fixes = failures.map(f => f.getFix()); 116 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 117 | 118 | let expected = ` 119 | @Component({ 120 | template: \` 121 | 122 | \` 123 | }) 124 | class Bar {} 125 | `; 126 | 127 | expect(res).to.eq(expected); 128 | }); 129 | 130 | it('should add slot="start" to ion-radio without slot on multiline', () => { 131 | let source = ` 132 | @Component({ 133 | template: \` 134 | 138 | \` 139 | }) 140 | class Bar {} 141 | `; 142 | 143 | const fail = { 144 | message: 'The slot attribute of ion-radio is required. Use slot="start".', 145 | startPosition: { 146 | line: 3, 147 | character: 13 148 | }, 149 | endPosition: { 150 | line: 3, 151 | character: 22 152 | } 153 | }; 154 | 155 | const failures = assertFailure(ruleName, source, fail); 156 | const fixes = failures.map(f => f.getFix()); 157 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 158 | 159 | let expected = ` 160 | @Component({ 161 | template: \` 162 | 166 | \` 167 | }) 168 | class Bar {} 169 | `; 170 | 171 | expect(res).to.eq(expected); 172 | }); 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/ionRangeAttributesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionRangeAttributesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 | 14 | 15 | \` 16 | }) 17 | class Bar{} 18 | `; 19 | assertSuccess(ruleName, source); 20 | }); 21 | }); 22 | 23 | describe('failure', () => { 24 | it('should fail when range-start is used', () => { 25 | let source = ` 26 | @Component({ 27 | template: \` 28 | 29 | 30 | ~~~~~~~~~~~ 31 | 32 | \` 33 | }) 34 | class Bar{} 35 | `; 36 | 37 | assertAnnotated({ 38 | ruleName, 39 | message: 'The range-start attribute of ion-icon has been renamed. Use slot="start" instead.', 40 | source 41 | }); 42 | }); 43 | 44 | it('should fail when range-left is used', () => { 45 | let source = ` 46 | @Component({ 47 | template: \` 48 | 49 | 50 | ~~~~~~~~~~ 51 | 52 | \` 53 | }) 54 | class Bar{} 55 | `; 56 | 57 | assertAnnotated({ 58 | ruleName, 59 | message: 'The range-left attribute of ion-icon has been renamed. Use slot="start" instead.', 60 | source 61 | }); 62 | }); 63 | 64 | it('should fail when range-end is used', () => { 65 | let source = ` 66 | @Component({ 67 | template: \` 68 | 69 | 70 | ~~~~~~~~~ 71 | 72 | \` 73 | }) 74 | class Bar{} 75 | `; 76 | 77 | assertAnnotated({ 78 | ruleName, 79 | message: 'The range-end attribute of ion-icon has been renamed. Use slot="end" instead.', 80 | source 81 | }); 82 | }); 83 | 84 | it('should fail when range-right is used', () => { 85 | let source = ` 86 | @Component({ 87 | template: \` 88 | 89 | 90 | ~~~~~~~~~~~ 91 | 92 | \` 93 | }) 94 | class Bar{} 95 | `; 96 | 97 | assertAnnotated({ 98 | ruleName, 99 | message: 'The range-right attribute of ion-icon has been renamed. Use slot="end" instead.', 100 | source 101 | }); 102 | }); 103 | 104 | it('should fail when used with any element', () => { 105 | let source = ` 106 | @Component({ 107 | template: \` 108 | 109 | 110 | ~~~~~~~~~~ 111 | 200 112 | ^^^^^^^^^^^ 113 | 114 | \` 115 | }) 116 | class Bar{} 117 | `; 118 | 119 | assertMultipleAnnotated({ 120 | ruleName, 121 | source, 122 | failures: [ 123 | { 124 | char: '~', 125 | msg: 'The range-left attribute of i has been renamed. Use slot="start" instead.' 126 | }, 127 | { 128 | char: '^', 129 | msg: 'The range-right attribute of ion-label has been renamed. Use slot="end" instead.' 130 | } 131 | ] 132 | }); 133 | }); 134 | }); 135 | 136 | describe('replacements', () => { 137 | it('should replace range-start with slot="start"', () => { 138 | let source = ` 139 | @Component({ 140 | template: \` 141 | 142 | 143 | 144 | \` 145 | }) 146 | class Bar {} 147 | `; 148 | 149 | const fail = { 150 | message: 'The range-start attribute of ion-icon has been renamed. Use slot="start" instead.', 151 | startPosition: { 152 | line: 4, 153 | character: 37 154 | }, 155 | endPosition: { 156 | line: 4, 157 | character: 48 158 | } 159 | }; 160 | 161 | const failures = assertFailure(ruleName, source, fail); 162 | const fixes = failures.map(f => f.getFix()); 163 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 164 | 165 | let expected = ` 166 | @Component({ 167 | template: \` 168 | 169 | 170 | 171 | \` 172 | }) 173 | class Bar {} 174 | `; 175 | 176 | expect(res).to.eq(expected); 177 | }); 178 | 179 | it('should replace range-left with slot="start"', () => { 180 | let source = ` 181 | @Component({ 182 | template: \` 183 | 184 | 185 | 186 | \` 187 | }) 188 | class Bar {} 189 | `; 190 | 191 | const fail = { 192 | message: 'The range-left attribute of ion-icon has been renamed. Use slot="start" instead.', 193 | startPosition: { 194 | line: 4, 195 | character: 37 196 | }, 197 | endPosition: { 198 | line: 4, 199 | character: 47 200 | } 201 | }; 202 | 203 | const failures = assertFailure(ruleName, source, fail); 204 | const fixes = failures.map(f => f.getFix()); 205 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 206 | 207 | let expected = ` 208 | @Component({ 209 | template: \` 210 | 211 | 212 | 213 | \` 214 | }) 215 | class Bar {} 216 | `; 217 | 218 | expect(res).to.eq(expected); 219 | }); 220 | 221 | it('should replace range-right with slot="end"', () => { 222 | let source = ` 223 | @Component({ 224 | template: \` 225 | 226 | 227 | 228 | \` 229 | }) 230 | class Bar {} 231 | `; 232 | 233 | const fail = { 234 | message: 'The range-right attribute of ion-icon has been renamed. Use slot="end" instead.', 235 | startPosition: { 236 | line: 4, 237 | character: 37 238 | }, 239 | endPosition: { 240 | line: 4, 241 | character: 48 242 | } 243 | }; 244 | 245 | const failures = assertFailure(ruleName, source, fail); 246 | const fixes = failures.map(f => f.getFix()); 247 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 248 | 249 | let expected = ` 250 | @Component({ 251 | template: \` 252 | 253 | 254 | 255 | \` 256 | }) 257 | class Bar {} 258 | `; 259 | 260 | expect(res).to.eq(expected); 261 | }); 262 | 263 | it('should replace range-end with slot="end"', () => { 264 | let source = ` 265 | @Component({ 266 | template: \` 267 | 268 | 269 | 270 | \` 271 | }) 272 | class Bar {} 273 | `; 274 | 275 | const fail = { 276 | message: 'The range-end attribute of ion-icon has been renamed. Use slot="end" instead.', 277 | startPosition: { 278 | line: 4, 279 | character: 37 280 | }, 281 | endPosition: { 282 | line: 4, 283 | character: 46 284 | } 285 | }; 286 | 287 | const failures = assertFailure(ruleName, source, fail); 288 | const fixes = failures.map(f => f.getFix()); 289 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 290 | 291 | let expected = ` 292 | @Component({ 293 | template: \` 294 | 295 | 296 | 297 | \` 298 | }) 299 | class Bar {} 300 | `; 301 | 302 | expect(res).to.eq(expected); 303 | }); 304 | }); 305 | }); 306 | -------------------------------------------------------------------------------- /test/ionSegmentButtonIonLabelRequired.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionSegmentButtonIonLabelRequiredRule'; 2 | import { assertAnnotated, assertSuccess } from './testHelper'; 3 | 4 | describe(ruleName, () => { 5 | describe('success', () => { 6 | it('should work with ion-label child', () => { 7 | let source = ` 8 | @Component({ 9 | template: \` 10 | 11 | Dog 12 | 13 | \` 14 | }) 15 | class Bar{} 16 | `; 17 | assertSuccess(ruleName, source); 18 | }); 19 | }); 20 | 21 | describe('failure', () => { 22 | it('should fail when ion-label missing', () => { 23 | let source = ` 24 | @Component({ 25 | template: \` 26 | 27 | ~~~~~~~~~~~~~~~~~~ 28 | Dog 29 | 30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | 35 | assertAnnotated({ 36 | ruleName, 37 | message: 'The ion-segment-button requires an ion-label component. It is no longer automatically added.', 38 | source 39 | }); 40 | }); 41 | 42 | it('should fail with only text', () => { 43 | let source = ` 44 | @Component({ 45 | template: \` 46 | Dog 47 | ~~~~~~~~~~~~~~~~~~ 48 | \` 49 | }) 50 | class Bar{} 51 | `; 52 | 53 | assertAnnotated({ 54 | ruleName, 55 | message: 'The ion-segment-button requires an ion-label component. It is no longer automatically added.', 56 | source 57 | }); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/ionSpinnerAttributeValuesRenamed.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { Replacement, Utils } from 'tslint'; 3 | import { ruleName } from '../src/ionSpinnerAttributeValuesRenamedRule'; 4 | import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style for name="lines"', () => { 9 | let source = ` 10 | @Component({ 11 | template: \`\` 12 | }) 13 | class Bar{} 14 | `; 15 | assertSuccess(ruleName, source); 16 | }); 17 | 18 | it('should work with proper style for name="lines-small"', () => { 19 | let source = ` 20 | @Component({ 21 | template: \`\` 22 | }) 23 | class Bar{} 24 | `; 25 | assertSuccess(ruleName, source); 26 | }); 27 | 28 | it('should work with proper style for ion-loading', () => { 29 | let source = ` 30 | @Component({ 31 | template: \`\` 32 | }) 33 | class Bar{} 34 | `; 35 | assertSuccess(ruleName, source); 36 | }); 37 | }); 38 | 39 | describe('failure', () => { 40 | it('should fail when name="ios" is used', () => { 41 | let source = ` 42 | @Component({ 43 | template: \` 44 | \` 45 | ~~~~~~~~~~ 46 | }) 47 | class Bar{} 48 | `; 49 | 50 | assertAnnotated({ 51 | ruleName, 52 | message: 'The name="ios" attribute/value of ion-spinner should be written as name="lines".', 53 | source 54 | }); 55 | }); 56 | 57 | it('should fail when name="ios-small" is used', () => { 58 | let source = ` 59 | @Component({ 60 | template: \` 61 | \` 62 | ~~~~~~~~~~~~~~~~ 63 | }) 64 | class Bar{} 65 | `; 66 | 67 | assertAnnotated({ 68 | ruleName, 69 | message: 'The name="ios-small" attribute/value of ion-spinner should be written as name="lines-small".', 70 | source 71 | }); 72 | }); 73 | 74 | it('should fail when name="ios" is used on ion-loading', () => { 75 | let source = ` 76 | @Component({ 77 | template: \` 78 | \` 79 | ~~~~~~~~~~ 80 | }) 81 | class Bar{} 82 | `; 83 | 84 | assertAnnotated({ 85 | ruleName, 86 | message: 'The name="ios" attribute/value of ion-loading should be written as name="lines".', 87 | source 88 | }); 89 | }); 90 | }); 91 | 92 | describe('replacements', () => { 93 | it('should replace name="ios" with name="lines"', () => { 94 | let source = ` 95 | @Component({ 96 | template: \` 97 | \` 98 | }) 99 | class Bar {} 100 | `; 101 | 102 | const fail = { 103 | message: 'The name="ios" attribute/value of ion-spinner should be written as name="lines".', 104 | startPosition: { 105 | line: 3, 106 | character: 25 107 | }, 108 | endPosition: { 109 | line: 3, 110 | character: 35 111 | } 112 | }; 113 | 114 | const failures = assertFailure(ruleName, source, fail); 115 | const fixes = failures.map(f => f.getFix()); 116 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 117 | 118 | let expected = ` 119 | @Component({ 120 | template: \` 121 | \` 122 | }) 123 | class Bar {} 124 | `; 125 | 126 | expect(res).to.eq(expected); 127 | }); 128 | 129 | it('should replace name="ios-small" with name="lines-small"', () => { 130 | let source = ` 131 | @Component({ 132 | template: \` 133 | \` 134 | }) 135 | class Bar {} 136 | `; 137 | 138 | const fail = { 139 | message: 'The name="ios-small" attribute/value of ion-spinner should be written as name="lines-small".', 140 | startPosition: { 141 | line: 3, 142 | character: 25 143 | }, 144 | endPosition: { 145 | line: 3, 146 | character: 41 147 | } 148 | }; 149 | 150 | const failures = assertFailure(ruleName, source, fail); 151 | const fixes = failures.map(f => f.getFix()); 152 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 153 | 154 | let expected = ` 155 | @Component({ 156 | template: \` 157 | \` 158 | }) 159 | class Bar {} 160 | `; 161 | 162 | expect(res).to.eq(expected); 163 | }); 164 | 165 | it('should replace name="ios" with name="lines" for ion-loading', () => { 166 | let source = ` 167 | @Component({ 168 | template: \` 169 | \` 170 | }) 171 | class Bar {} 172 | `; 173 | 174 | const fail = { 175 | message: 'The name="ios" attribute/value of ion-loading should be written as name="lines".', 176 | startPosition: { 177 | line: 3, 178 | character: 25 179 | }, 180 | endPosition: { 181 | line: 3, 182 | character: 35 183 | } 184 | }; 185 | 186 | const failures = assertFailure(ruleName, source, fail); 187 | const fixes = failures.map(f => f.getFix()); 188 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 189 | 190 | let expected = ` 191 | @Component({ 192 | template: \` 193 | \` 194 | }) 195 | class Bar {} 196 | `; 197 | 198 | expect(res).to.eq(expected); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /test/ionTabsRefactored.spec.ts: -------------------------------------------------------------------------------- 1 | import { ruleName } from '../src/ionTabsRefactoredRule'; 2 | import { assertAnnotated } from './testHelper'; 3 | describe(ruleName, () => { 4 | describe('failure', () => { 5 | it('should fail when ion-tabs are used', () => { 6 | let source = ` 7 | @Component({ 8 | template: \` 9 | 10 | ~~~~~~~~ 11 | 12 | 13 | \` 14 | }) 15 | class Bar{} 16 | `; 17 | assertAnnotated({ 18 | ruleName, 19 | message: `Tabs have gone through a significant refactor. 20 | Please see https://github.com/ionic-team/ionic/blob/master/CHANGELOG.md#angular-tabs`, 21 | source 22 | }); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/ionTextIsNowAnElement.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { ruleName } from '../src/ionTextIsNowAnElementRule'; 3 | import { assertAnnotated, assertMultipleAnnotated, assertSuccess, assertFailure } from './testHelper'; 4 | import { Replacement, Utils } from 'tslint'; 5 | 6 | describe(ruleName, () => { 7 | describe('success', () => { 8 | it('should work with proper style', () => { 9 | let source = ` 10 | @Component({ 11 | template: \` 12 | 13 |

H1: The quick brown fox jumps over the lazy dog

14 |
15 | 16 | 17 |

H2: The quick brown fox jumps over the lazy dog

18 |
19 | 20 | 21 |

H3: The quick brown fox jumps over the lazy dog

22 |
23 | 24 |

25 | I saw a werewolf with a Chinese menu in his hand. 26 | Walking through the streets of Soho in the rain. 27 | He was looking for a place called Lee Ho Fook's. 28 | Gonna get a big dish of beef chow mein. 29 |

30 | \` 31 | }) 32 | class Bar{} 33 | `; 34 | assertSuccess(ruleName, source); 35 | }); 36 | }); 37 | 38 | describe('failure', () => { 39 | it('should fail when ion-text attribute is used on h1', () => { 40 | let source = ` 41 | @Component({ 42 | template: \` 43 |

H1: The quick brown fox jumps over the lazy dog

44 | ~~~~~~~~ 45 | \` 46 | }) 47 | class Bar{} 48 | `; 49 | 50 | assertAnnotated({ 51 | ruleName, 52 | message: 'ion-text is now an ion-text element instead of an Angular directive.', 53 | source 54 | }); 55 | }); 56 | 57 | it('should fail when ion-text attribute is used on other elements', () => { 58 | let source = ` 59 | @Component({ 60 | template: \` 61 |

62 | I saw a werewolf with a Chinese menu in his hand. 63 | Walking through the streets of Soho in the rain. 64 | ~~~~~~~~ 65 | He was looking for a place called Lee Ho Fook's. 66 | ^^^^^^^^ 67 | Gonna get a big dish of beef chow mein. 68 | 11111111 69 |

70 | \` 71 | }) 72 | class Bar{} 73 | `; 74 | 75 | assertMultipleAnnotated({ 76 | ruleName, 77 | source, 78 | failures: [ 79 | { 80 | char: '~', 81 | msg: 'ion-text is now an ion-text element instead of an Angular directive.' 82 | }, 83 | { 84 | char: '^', 85 | msg: 'ion-text is now an ion-text element instead of an Angular directive.' 86 | }, 87 | { 88 | char: '1', 89 | msg: 'ion-text is now an ion-text element instead of an Angular directive.' 90 | } 91 | ] 92 | }); 93 | }); 94 | }); 95 | 96 | describe('replacements', () => { 97 | it('should create parent ion-text and remove attribute', () => { 98 | let source = ` 99 | @Component({ 100 | template: \`

101 | \` 102 | }) 103 | class Bar {} 104 | `; 105 | const fail = { 106 | message: 'ion-text is now an ion-text element instead of an Angular directive.', 107 | startPosition: { 108 | line: 2, 109 | character: 25 110 | }, 111 | endPosition: { 112 | line: 2, 113 | character: 33 114 | } 115 | }; 116 | 117 | const failures = assertFailure(ruleName, source, fail); 118 | const fixes = failures.map(f => f.getFix()); 119 | const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); 120 | 121 | let expected = ` 122 | @Component({ 123 | template: \` 124 |

125 |
126 | \` 127 | }) 128 | class Bar {} 129 | `; 130 | expect(res).to.eq(expected); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { IOptions, IRule } from 'tslint'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | 5 | export function convertRuleOptions(ruleConfiguration: Map>): IOptions[] { 6 | const output: IOptions[] = []; 7 | ruleConfiguration.forEach(({ ruleArguments, ruleSeverity }, ruleName) => { 8 | const options: IOptions = { 9 | disabledIntervals: [], // deprecated, so just provide an empty array. 10 | ruleArguments: ruleArguments !== null ? ruleArguments : [], 11 | ruleName, 12 | ruleSeverity: ruleSeverity !== null ? ruleSeverity : 'error' 13 | }; 14 | output.push(options); 15 | }); 16 | return output; 17 | } 18 | 19 | const cachedRules = new Map(); 20 | 21 | export function camelize(stringWithHyphens: string): string { 22 | return stringWithHyphens.replace(/-(.)/g, (_, nextLetter) => (nextLetter as string).toUpperCase()); 23 | } 24 | 25 | function transformName(name: string): string { 26 | // camelize strips out leading and trailing underscores and dashes, so make sure they aren't passed to camelize 27 | // the regex matches the groups (leading underscores and dashes)(other characters)(trailing underscores and dashes) 28 | const nameMatch = name.match(/^([-_]*)(.*?)([-_]*)$/); 29 | if (nameMatch === null) { 30 | return `${name}Rule`; 31 | } 32 | return `${nameMatch[1]}${camelize(nameMatch[2])}${nameMatch[3]}Rule`; 33 | } 34 | 35 | /** 36 | * @param directory - An absolute path to a directory of rules 37 | * @param ruleName - A name of a rule in filename format. ex) "someLintRule" 38 | */ 39 | function loadRule(directory: string, ruleName: string): any | 'not-found' { 40 | const fullPath = path.join(directory, ruleName); 41 | if (fs.existsSync(`${fullPath}.js`)) { 42 | const ruleModule = require(fullPath) as { Rule: any } | undefined; 43 | if (ruleModule !== undefined) { 44 | return ruleModule.Rule; 45 | } 46 | } 47 | return 'not-found'; 48 | } 49 | 50 | export function getRelativePath(directory?: string | null, relativeTo?: string) { 51 | if (directory !== null) { 52 | const basePath = relativeTo !== undefined ? relativeTo : process.cwd(); 53 | return path.resolve(basePath, directory); 54 | } 55 | return undefined; 56 | } 57 | 58 | export function arrayify(arg?: T | T[]): T[] { 59 | if (Array.isArray(arg)) { 60 | return arg; 61 | } else if (arg !== null) { 62 | return [arg]; 63 | } else { 64 | return []; 65 | } 66 | } 67 | 68 | function loadCachedRule(directory: string, ruleName: string, isCustomPath?: boolean): any | undefined { 69 | // use cached value if available 70 | const fullPath = path.join(directory, ruleName); 71 | const cachedRule = cachedRules.get(fullPath); 72 | if (cachedRule !== undefined) { 73 | return cachedRule === 'not-found' ? undefined : cachedRule; 74 | } 75 | 76 | // get absolute path 77 | let absolutePath: string | undefined = directory; 78 | if (isCustomPath) { 79 | absolutePath = getRelativePath(directory); 80 | if (absolutePath !== undefined && !fs.existsSync(absolutePath)) { 81 | throw new Error(`Could not find custom rule directory: ${directory}`); 82 | } 83 | } 84 | 85 | const Rule = absolutePath === undefined ? 'not-found' : loadRule(absolutePath, ruleName); 86 | 87 | cachedRules.set(fullPath, Rule); 88 | return Rule === 'not-found' ? undefined : Rule; 89 | } 90 | 91 | export function find(inputs: T[], getResult: (t: T) => U | undefined): U | undefined { 92 | for (const element of inputs) { 93 | const result = getResult(element); 94 | if (result !== undefined) { 95 | return result; 96 | } 97 | } 98 | return undefined; 99 | } 100 | 101 | function findRule(name: string, rulesDirectories?: string | string[]): any | undefined { 102 | const camelizedName = transformName(name); 103 | return find(arrayify(rulesDirectories), dir => loadCachedRule(dir, camelizedName, true)); 104 | } 105 | 106 | export function loadRules(ruleOptionsList: IOptions[], rulesDirectories?: string | string[], isJs = false): IRule[] { 107 | const rules: IRule[] = []; 108 | const notFoundRules: string[] = []; 109 | const notAllowedInJsRules: string[] = []; 110 | 111 | for (const ruleOptions of ruleOptionsList) { 112 | if (ruleOptions.ruleSeverity === 'off') { 113 | // Perf: don't bother finding the rule if it's disabled. 114 | continue; 115 | } 116 | 117 | const ruleName = ruleOptions.ruleName; 118 | const Rule = findRule(ruleName, rulesDirectories); 119 | if (Rule === undefined) { 120 | notFoundRules.push(ruleName); 121 | } else if (isJs && Rule.metadata !== undefined && Rule.metadata.typescriptOnly) { 122 | notAllowedInJsRules.push(ruleName); 123 | } else { 124 | const rule = new Rule(ruleOptions); 125 | if (rule.isEnabled()) { 126 | rules.push(rule); 127 | } 128 | } 129 | } 130 | return rules; 131 | } 132 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "outDir": "./rules", 7 | "pretty": true, 8 | "strict": false, 9 | "target": "es6", 10 | "lib": [ 11 | "es2016" 12 | ] 13 | }, 14 | "include": [ 15 | "src/**/*.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "pretty": true, 7 | "strict": false, 8 | "target": "es6", 9 | "lib": [ 10 | "es2016" 11 | ] 12 | }, 13 | "include": [ 14 | "test/**/*.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "member-ordering": [ 5 | true, 6 | { 7 | "order": [ 8 | "public-static-field", 9 | "public-static-method", 10 | "protected-static-field", 11 | "protected-static-method", 12 | "private-static-field", 13 | "private-static-method", 14 | "public-instance-field", 15 | "protected-instance-field", 16 | "private-instance-field", 17 | "public-constructor", 18 | "protected-constructor", 19 | "private-constructor", 20 | "public-instance-method", 21 | "protected-instance-method", 22 | "private-instance-method" 23 | ] 24 | } 25 | ], 26 | "no-arg": true, 27 | "no-construct": true, 28 | "no-duplicate-variable": true, 29 | "no-eval": true, 30 | "no-unused-expression": true, 31 | "no-var-keyword": true, 32 | "triple-equals": true, 33 | "variable-name": false, 34 | "max-line-length": [true, 140] 35 | } 36 | } 37 | --------------------------------------------------------------------------------