forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add undecorated classes with decorated fields schematic (a…
…ngular#32130) Adds a schematic that adds a `Directive` decorator to undecorated classes that have fields that use Angular decorators. PR Close angular#32130
- Loading branch information
Showing
11 changed files
with
720 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
packages/core/schematics/migrations/google3/undecoratedClassesWithDecoratedFieldsRule.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Replacement, RuleFailure, Rules} from 'tslint'; | ||
import * as ts from 'typescript'; | ||
|
||
import {FALLBACK_DECORATOR, addImport, getNamedImports, getUndecoratedClassesWithDecoratedFields, hasNamedImport} from '../undecorated-classes-with-decorated-fields/utils'; | ||
|
||
|
||
|
||
/** | ||
* TSLint rule that adds an Angular decorator to classes that have Angular field decorators. | ||
* https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA | ||
*/ | ||
export class Rule extends Rules.TypedRule { | ||
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { | ||
const typeChecker = program.getTypeChecker(); | ||
const printer = ts.createPrinter(); | ||
const classes = getUndecoratedClassesWithDecoratedFields(sourceFile, typeChecker); | ||
|
||
return classes.map((current, index) => { | ||
const {classDeclaration: declaration, importDeclaration} = current; | ||
const name = declaration.name; | ||
|
||
// Set the class identifier node (if available) as the failing node so IDEs don't highlight | ||
// the entire class with red. This is similar to how errors are shown for classes in other | ||
// cases like an interface not being implemented correctly. | ||
const start = (name || declaration).getStart(); | ||
const end = (name || declaration).getEnd(); | ||
const fixes = [Replacement.appendText(declaration.getStart(), `@${FALLBACK_DECORATOR}()\n`)]; | ||
|
||
// If it's the first class that we're processing in this file, add `Directive` to the imports. | ||
if (index === 0 && !hasNamedImport(importDeclaration, FALLBACK_DECORATOR)) { | ||
const namedImports = getNamedImports(importDeclaration); | ||
|
||
if (namedImports) { | ||
fixes.push(new Replacement( | ||
namedImports.getStart(), namedImports.getWidth(), | ||
printer.printNode( | ||
ts.EmitHint.Unspecified, addImport(namedImports, FALLBACK_DECORATOR), | ||
sourceFile))); | ||
} | ||
} | ||
|
||
return new RuleFailure( | ||
sourceFile, start, end, | ||
'Classes with decorated fields must have an Angular decorator as well.', | ||
'undecorated-classes-with-decorated-fields', fixes); | ||
}); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
load("//tools:defaults.bzl", "ts_library") | ||
|
||
ts_library( | ||
name = "undecorated-classes-with-decorated-fields", | ||
srcs = glob(["**/*.ts"]), | ||
tsconfig = "//packages/core/schematics:tsconfig.json", | ||
visibility = [ | ||
"//packages/core/schematics:__pkg__", | ||
"//packages/core/schematics/migrations/google3:__pkg__", | ||
"//packages/core/schematics/test:__pkg__", | ||
], | ||
deps = [ | ||
"//packages/core/schematics/utils", | ||
"@npm//@angular-devkit/schematics", | ||
"@npm//@types/node", | ||
"@npm//typescript", | ||
], | ||
) |
23 changes: 23 additions & 0 deletions
23
.../core/schematics/migrations/undecorated-classes-with-decorated-fields/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
## Undecorated classes with decorated fields migration | ||
|
||
Automatically adds a `Directive` decorator to undecorated classes that have fields with Angular | ||
decorators. Also adds the relevant imports, if necessary. | ||
|
||
#### Before | ||
```ts | ||
import { Input } from '@angular/core'; | ||
|
||
export class Base { | ||
@Input() isActive: boolean; | ||
} | ||
``` | ||
|
||
#### After | ||
```ts | ||
import { Input, Directive } from '@angular/core'; | ||
|
||
@Directive() | ||
export class Base { | ||
@Input() isActive: boolean; | ||
} | ||
``` |
93 changes: 93 additions & 0 deletions
93
packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Rule, SchematicContext, SchematicsException, Tree} from '@angular-devkit/schematics'; | ||
import {dirname, relative} from 'path'; | ||
import * as ts from 'typescript'; | ||
|
||
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; | ||
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig'; | ||
import {FALLBACK_DECORATOR, addImport, getNamedImports, getUndecoratedClassesWithDecoratedFields, hasNamedImport} from './utils'; | ||
|
||
|
||
/** | ||
* Migration that adds an Angular decorator to classes that have Angular field decorators. | ||
* https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA | ||
*/ | ||
export default function(): Rule { | ||
return (tree: Tree, context: SchematicContext) => { | ||
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree); | ||
const basePath = process.cwd(); | ||
const allPaths = [...buildPaths, ...testPaths]; | ||
const logger = context.logger; | ||
|
||
logger.info('------ Undecorated classes with decorated fields migration ------'); | ||
logger.info( | ||
'As of Angular 9, it is no longer supported to have Angular field ' + | ||
'decorators on a class that does not have an Angular decorator.'); | ||
|
||
if (!allPaths.length) { | ||
throw new SchematicsException( | ||
'Could not find any tsconfig file. Cannot add an Angular decorator to undecorated classes.'); | ||
} | ||
|
||
for (const tsconfigPath of allPaths) { | ||
runUndecoratedClassesMigration(tree, tsconfigPath, basePath); | ||
} | ||
}; | ||
} | ||
|
||
function runUndecoratedClassesMigration(tree: Tree, tsconfigPath: string, basePath: string) { | ||
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath)); | ||
const host = ts.createCompilerHost(parsed.options, true); | ||
|
||
// We need to overwrite the host "readFile" method, as we want the TypeScript | ||
// program to be based on the file contents in the virtual file tree. Otherwise | ||
// if we run the migration for multiple tsconfig files which have intersecting | ||
// source files, it can end up updating them multiple times. | ||
host.readFile = fileName => { | ||
const buffer = tree.read(relative(basePath, fileName)); | ||
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which | ||
// which breaks the CLI UpdateRecorder. | ||
// See: https://github.com/angular/angular/pull/30719 | ||
return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined; | ||
}; | ||
|
||
const program = ts.createProgram(parsed.fileNames, parsed.options, host); | ||
const typeChecker = program.getTypeChecker(); | ||
const printer = ts.createPrinter(); | ||
const sourceFiles = program.getSourceFiles().filter( | ||
file => !file.isDeclarationFile && !program.isSourceFileFromExternalLibrary(file)); | ||
|
||
sourceFiles.forEach(sourceFile => { | ||
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); | ||
const classes = getUndecoratedClassesWithDecoratedFields(sourceFile, typeChecker); | ||
|
||
classes.forEach((current, index) => { | ||
// If it's the first class that we're processing in this file, add `Directive` to the imports. | ||
if (index === 0 && !hasNamedImport(current.importDeclaration, FALLBACK_DECORATOR)) { | ||
const namedImports = getNamedImports(current.importDeclaration); | ||
|
||
if (namedImports) { | ||
update.remove(namedImports.getStart(), namedImports.getWidth()); | ||
update.insertRight( | ||
namedImports.getStart(), | ||
printer.printNode( | ||
ts.EmitHint.Unspecified, addImport(namedImports, FALLBACK_DECORATOR), | ||
sourceFile)); | ||
} | ||
} | ||
|
||
// We don't need to go through the AST to insert the decorator, because the change | ||
// is pretty basic. Also this has a better chance of preserving the user's formatting. | ||
update.insertLeft(current.classDeclaration.getStart(), `@${FALLBACK_DECORATOR}()\n`); | ||
}); | ||
|
||
tree.commitUpdate(update); | ||
}); | ||
} |
71 changes: 71 additions & 0 deletions
71
packages/core/schematics/migrations/undecorated-classes-with-decorated-fields/utils.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
/** | ||
* @license | ||
* Copyright Google Inc. All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import * as ts from 'typescript'; | ||
import {getAngularDecorators} from '../../utils/ng_decorators'; | ||
|
||
/** Name of the decorator that should be added to undecorated classes. */ | ||
export const FALLBACK_DECORATOR = 'Directive'; | ||
|
||
/** Finds all of the undecorated classes that have decorated fields within a file. */ | ||
export function getUndecoratedClassesWithDecoratedFields( | ||
sourceFile: ts.SourceFile, typeChecker: ts.TypeChecker) { | ||
const classes: UndecoratedClassWithDecoratedFields[] = []; | ||
|
||
sourceFile.forEachChild(function walk(node: ts.Node) { | ||
if (ts.isClassDeclaration(node) && | ||
(!node.decorators || !getAngularDecorators(typeChecker, node.decorators).length)) { | ||
for (const member of node.members) { | ||
const angularDecorators = | ||
member.decorators && getAngularDecorators(typeChecker, member.decorators); | ||
|
||
if (angularDecorators && angularDecorators.length) { | ||
classes.push( | ||
{classDeclaration: node, importDeclaration: angularDecorators[0].importNode}); | ||
return; | ||
} | ||
} | ||
} | ||
|
||
node.forEachChild(walk); | ||
}); | ||
|
||
return classes; | ||
} | ||
|
||
/** Checks whether an import declaration has an import with a certain name. */ | ||
export function hasNamedImport(declaration: ts.ImportDeclaration, symbolName: string): boolean { | ||
const namedImports = getNamedImports(declaration); | ||
|
||
if (namedImports) { | ||
return namedImports.elements.some(element => { | ||
const {name, propertyName} = element; | ||
return propertyName ? propertyName.text === symbolName : name.text === symbolName; | ||
}); | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** Extracts the NamedImports node from an import declaration. */ | ||
export function getNamedImports(declaration: ts.ImportDeclaration): ts.NamedImports|null { | ||
const namedBindings = declaration.importClause && declaration.importClause.namedBindings; | ||
return (namedBindings && ts.isNamedImports(namedBindings)) ? namedBindings : null; | ||
} | ||
|
||
/** Adds a new import to a NamedImports node. */ | ||
export function addImport(declaration: ts.NamedImports, symbolName: string) { | ||
return ts.updateNamedImports(declaration, [ | ||
...declaration.elements, ts.createImportSpecifier(undefined, ts.createIdentifier(symbolName)) | ||
]); | ||
} | ||
|
||
interface UndecoratedClassWithDecoratedFields { | ||
classDeclaration: ts.ClassDeclaration; | ||
importDeclaration: ts.ImportDeclaration; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.