-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Extensibility model #9038
RFC: Extensibility model #9038
Conversation
@mhegazy Would you like to look over this? |
@billti You should take a look at this, too. |
d077396
to
f1b8fe0
Compare
f1b8fe0
to
8b3aeb8
Compare
@alexeagle I am. I was asked to start working on this a bit more than two weeks ago. ;) |
I'm also planning on proposing an external API for building types, to pair with the language service extensions, type provider extensions (more of a prerequisite), and semantic lint extensions - this way you don't have to inject new files of types into a compilation (usually meaning re-running the compilation) just to perform type comparisons. |
Other extension points that Angular needs:
I wrote more details: #3003 (comment) |
This looks very neat! I pulled these examples of a Lint extension out of your PR for ease of viewing: import {SemanticLintWalker} from "typescript-plugin-api";
export default class IsValueFoo extends SemanticLintWalker {
constructor(state) { super(state); }
visit(node, stop, error) {
const type = this.checker.getTypeAtLocation(node);
if (type.flags & this.ts.TypeFlags.StringLiteral) {
if (node.text === "foo") {
error("String literal type 'foo' is forbidden.", node);
}
}
}
} import {SyntacticLintWalker} from "typescript-plugin-api";
export default class IsNamedFoo extends SyntacticLintWalker {
constructor(state) { super(state); }
visit(node, stop, error) {
if (node.kind === this.ts.SyntaxKind.Identifier) {
if (node.text.toLowerCase() === "foo") {
error("Identifier 'foo' is forbidden.", node);
}
}
}
} |
Skimming over this briefly, I didn't see an example of how an end user of an extension would be able to configure an extension, but it looks like from the code that it would be something like this in a
|
} | ||
|
||
function error(err: string, node: Node) { | ||
diagnostics.push(createExtensionDiagnosticForNode(node, activeLint.name, err)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are times when an error might not appropriately be matched to a specific node. Is it worth considering a method of specifying an error that doesn't correspond to a single node?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh. I can just make node optional and emit a general program diagnostic if it's not present.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a reasonably easy way to provide the start
and length
of the added Diagnostic
should give the appropriate amount of flexibility I think. Being able to provide a node (like the current implementation) seems like a special case of above, but on that's worth providing first-class support for as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function error(err: string);
function error(err: string, node: Node);
function error(err: string, start: number, length: number);
function error(err: string, nodeOrStart?: Node | number, length?: number) {
//...
}
seems appropriate.
@jkillian That would be correct. This should also be valid (if you don't need/want to pass any arguments to your extensions): |
@@ -17258,6 +17258,10 @@ namespace ts { | |||
return getTypeForVariableLikeDeclaration(<VariableLikeDeclaration>node.parent, /*includeOptionality*/ true); | |||
} | |||
|
|||
if (node.kind === SyntaxKind.SourceFile) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the file is a module, we should return the module symbol.
return; // Avoid errors on explicitly exported null/undefined (why would someone do that, though?) | ||
} | ||
const annotatedKind = potentialExtension["extension-kind"]; | ||
if (typeof annotatedKind === "string") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
annotatedKind [](start = 35, length = 13)
when is this guard ever false? I thought "extension-kind" had a string literal union type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're reading it from untrusted JS. So it's always possible (at run time). This is where the canonical extension object is "made", so we need to guard against exceptional types here.
Debug.fail("If only one argument is passed to error, it must be the error message string."); | ||
return; | ||
} | ||
// error(err: string): void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does this comment belong here any more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes - I was placing comments with the corresponding overload next to their implementation, just for clarity.
@sandersn I think you'll be much more pleased with the |
// Overrides | ||
|
||
// A plugin can implement one of the override methods to replace the results that would | ||
// be returned by the TypeScript language service. If a plugin returns a defined results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
results [](start = 89, length = 7)
typo: result
// program. The value passed in as previous is the result returned by the prior plugin. If a | ||
// plugin returns undefined, the result passed in as previous is used and the undefined | ||
// result is ignored. All plugins are consulted before the result is returned to the host. | ||
// If a plugin overrides behavior of the method, no filter methods are consulted. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a plugin overrides behavior of the method, no filter methods are consulted. [](start = 10, length = 79)
I'm confused by the relationship of override and filter methods. Are overrides and filters called in the same sequence? It sounds like they are, with the first override aborting the cascade of the previous filters (but not throwing away their filtered values).
- What happens if a plugin has both an override and a filter?
- Could the whole thing be simplified to just have filter that have some way to signal that they should be the last plugin in the cascade -- that is, that they are an override? (This would dodge (1) by only allowing one of each on a class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an override returns a non-undefined
value, no further overrides are queried, no filters are queried. and the value is returned. If no override returns a non-undefined
value, then the base service is queried for a value. This value is then piped through the filter chain. If a filter returns undefined
, the previous value is used as input to the next filter instead. The result of the last non-undefined
returning filter is the final result, returned by the service.
A plugin can have both an override and a filter because it may, at times, wish to override everything (and at other times returning undefined
and not providing a value) and at others filter
(assuming it returned undefined
for the same operation in its override method).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So overrides are before (or replace) and filters are after, but overrides stop filters from running?
I'd suggest two things:
- Document overrides and filters together, plus add an example or two to show how they interact. (Maybe even change the names.)
- Consider making it so that overrides don't stop filters. Wouldn't it be useful for other plugins to be able to filter the output of a previous plugin?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Okay.
- @chuckjaz had the original API idea. Unless he has some strong objection to this, I'm okay with always having filters run.
62c8c17
to
805bc2f
Compare
After some discussion within the team, I'm going to split this PR back into three separate PRs: One for loading extensions, one for lint extensions, and one for LS extensions. We're generally find with loading extensions and how that works, however we have some contention within the team as to what extensions have value and what risks we're willing to take with them. I'm going to close this PR and open up three new ones shortly. |
Cc @TheLarkInn to track this incredible awesomeness. We could test this for webpack also. |
We use our internal module resolution logic to resolve the location of the entrypoint of a plugin from the compilation, then pass that path off to the host to have it load the code. For our
node
host, this is just arequire
call. We then look at the exported values of this module and check for members annotated with the private member["extension-kind"]
which is intended to be one of the string literal values defined as validExtensionKind
s. Later on, the loaded extensions can be retrieved and constructed when required, then have their methods called.Please note: The extension API is not included anywhere in the compiler - however, an example of how to implement a package for using the extension API is given in the tests. It's my recommendation that the extension API be a separate package from TS proper, since the 'global'
tsc
needs to use the 'local' extension, and if that local extension has its own bundled typescript, then version mismatching can happen unexpectedly.Additionally, to serve as a testbed for extension loading (and to implement a frequently asked for feature), single-pass semantic and syntactic lint extensions are also in this PR.
Thoughts:
loadExtension
method on other hosts similarly to how I've implemented it in the tests (if that is desirable). This is pretty much amounts to building a commonjs module loader into the compiler.Future work: