Skip to content

Commit debcf30

Browse files
authored
Merge pull request #41 from PolymerLabs/distinguish
Use type checker to identify msg function and lit-localize module
2 parents 103cb7f + 382eb4f commit debcf30

File tree

6 files changed

+205
-132
lines changed

6 files changed

+205
-132
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66

77
The `lit-localize` module exports the following functions:
88

9+
> Note that lit-localize relies on distinctive, annotated TypeScript type
10+
> signatures to identify calls to `msg` and other APIs during analysis of your
11+
> code. Casting a lit-localize function to a type that does not include its
12+
> annotation will prevent lit-localize from being able to extract and transform
13+
> templates from your application. For example, a cast like
14+
> `(msg as any)("greeting", "Hello")` will not be identified. It is safe to
15+
> re-assign lit-localize functions or pass them as parameters, as long as the
16+
> distinctive type signature is preserved. If needed, you can reference each
17+
> function's distinctive type with e.g. `typeof msg`.
18+
919
### `configureLocalization(configuration)`
1020

1121
Set configuration parameters for lit-localize when in runtime mode. Returns an

src/outputters/transform.ts

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export function transformOutput(
5252
}
5353
opts.outDir = pathLib.join(outRoot, '/', locale);
5454
program.emit(undefined, undefined, undefined, undefined, {
55-
before: [litLocalizeTransform(translations)],
55+
before: [litLocalizeTransform(translations, program)],
5656
});
5757
}
5858
}
@@ -61,10 +61,11 @@ export function transformOutput(
6161
* Return a TypeScript TransformerFactory for the lit-localize transformer.
6262
*/
6363
export function litLocalizeTransform(
64-
translations: Map<string, Message> | undefined
64+
translations: Map<string, Message> | undefined,
65+
program: ts.Program
6566
): ts.TransformerFactory<ts.SourceFile> {
6667
return (context) => {
67-
const transformer = new Transformer(context, translations);
68+
const transformer = new Transformer(context, translations, program);
6869
return (file) => ts.visitNode(file, transformer.boundVisitNode);
6970
};
7071
}
@@ -75,21 +76,24 @@ export function litLocalizeTransform(
7576
class Transformer {
7677
private context: ts.TransformationContext;
7778
private translations: Map<string, Message> | undefined;
79+
private typeChecker: ts.TypeChecker;
7880
boundVisitNode = this.visitNode.bind(this);
7981

8082
constructor(
8183
context: ts.TransformationContext,
82-
translations: Map<string, Message> | undefined
84+
translations: Map<string, Message> | undefined,
85+
program: ts.Program
8386
) {
8487
this.context = context;
8588
this.translations = translations;
89+
this.typeChecker = program.getTypeChecker();
8690
}
8791

8892
/**
8993
* Top-level delegating visitor for all nodes.
9094
*/
9195
visitNode(node: ts.Node): ts.VisitResult<ts.Node> {
92-
if (isMsgCall(node)) {
96+
if (isMsgCall(node, this.typeChecker)) {
9397
return this.replaceMsgCall(node);
9498
}
9599
if (isLitTemplate(node)) {
@@ -101,8 +105,8 @@ class Transformer {
101105
)
102106
);
103107
}
104-
if (ts.isImportDeclaration(node)) {
105-
return this.removeMsgImport(node);
108+
if (this.isLitLocalizeImport(node)) {
109+
return undefined;
106110
}
107111
return ts.visitEachChild(node, this.boundVisitNode, this.context);
108112
}
@@ -329,29 +333,29 @@ class Transformer {
329333
}
330334

331335
/**
332-
* Remove import declarations for the lit-localize `msg` function, because we
333-
* are transforming away all calls to that function.
336+
* Return whether the given node is an import for the lit-localize module.
334337
*/
335-
removeMsgImport(
336-
imprt: ts.ImportDeclaration
337-
): ts.ImportDeclaration | undefined {
338-
const clause = imprt.importClause;
339-
if (clause === undefined) {
340-
return imprt;
338+
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
339+
if (!ts.isImportDeclaration(node)) {
340+
return false;
341341
}
342-
const bindings = clause.namedBindings;
343-
if (bindings === undefined || !ts.isNamedImports(bindings)) {
344-
return imprt;
342+
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
343+
node.moduleSpecifier
344+
);
345+
if (!moduleSymbol || !moduleSymbol.exports) {
346+
return false;
345347
}
346-
// TODO(aomarks) This is too crude. We should do better to identify only our
347-
// `msg` function.
348-
if (
349-
bindings.elements.length === 1 &&
350-
bindings.elements[0].name.text === 'msg'
351-
) {
352-
return undefined;
348+
const exports = moduleSymbol.exports.values();
349+
for (const xport of exports as typeof exports & {
350+
[Symbol.iterator](): Iterator<ts.Symbol>;
351+
}) {
352+
const type = this.typeChecker.getTypeAtLocation(xport.valueDeclaration);
353+
const props = this.typeChecker.getPropertiesOfType(type);
354+
if (props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_')) {
355+
return true;
356+
}
353357
}
354-
return imprt;
358+
return false;
355359
}
356360
}
357361

src/program-analysis.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import {createDiagnostic} from './typescript';
1818
* Extract translation messages from all files in a TypeScript program.
1919
*/
2020
export function extractMessagesFromProgram(
21-
node: ts.Program
21+
program: ts.Program
2222
): {messages: ProgramMessage[]; errors: ts.Diagnostic[]} {
2323
const messages: ProgramMessage[] = [];
2424
const errors: ts.Diagnostic[] = [];
25-
for (const sourcefile of node.getSourceFiles()) {
25+
for (const sourcefile of program.getSourceFiles()) {
2626
extractMessagesFromNode(sourcefile, sourcefile, messages, errors, []);
2727
}
2828
const deduped = dedupeMessages(messages);
@@ -493,14 +493,23 @@ export function isLitTemplate(
493493
/**
494494
* Return whether this is a call to the lit-localize `msg` function.
495495
*/
496-
export function isMsgCall(node: ts.Node): node is ts.CallExpression {
497-
// TODO(aomarks) This is too crude. We should do better to identify only our
498-
// `msg` function.
499-
return (
500-
ts.isCallExpression(node) &&
501-
ts.isIdentifier(node.expression) &&
502-
node.expression.escapedText === 'msg'
503-
);
496+
export function isMsgCall(
497+
node: ts.Node,
498+
typeChecker?: ts.TypeChecker
499+
): node is ts.CallExpression {
500+
if (!ts.isCallExpression(node)) {
501+
return false;
502+
}
503+
if (typeChecker === undefined) {
504+
// TODO(aomarks) Remove this branch once migration to static lit-localize
505+
// library is done.
506+
return (
507+
ts.isIdentifier(node.expression) && node.expression.escapedText === 'msg'
508+
);
509+
}
510+
const type = typeChecker.getTypeAtLocation(node.expression);
511+
const props = typeChecker.getPropertiesOfType(type);
512+
return props.some((prop) => prop.escapedName === '_LIT_LOCALIZE_MSG_');
504513
}
505514

506515
/**

src/tests/compile-ts-fragment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function compileTsFragment(
4242
inputCode: string,
4343
options: ts.CompilerOptions,
4444
cache: CompilerHostCache,
45-
transformers?: ts.CustomTransformers
45+
transformers?: (program: ts.Program) => ts.CustomTransformers
4646
): CompileResult {
4747
const dummyTsFilename = '__DUMMY__.ts';
4848
const dummyJsFilename = '__DUMMY__.js';
@@ -124,7 +124,7 @@ export function compileTsFragment(
124124
undefined,
125125
undefined,
126126
undefined,
127-
transformers
127+
transformers ? transformers(program) : undefined
128128
);
129129
return {
130130
code: outputCode,

0 commit comments

Comments
 (0)