Skip to content

Commit 9d30b42

Browse files
committed
Use type checker to identify msg function and lit-localize module
1 parent 372f9c2 commit 9d30b42

File tree

5 files changed

+116
-58
lines changed

5 files changed

+116
-58
lines changed

src/outputters/transform.ts

Lines changed: 32 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 (isLitExpression(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
}
@@ -328,29 +332,31 @@ class Transformer {
328332
}
329333

330334
/**
331-
* Remove import declarations for the lit-localize `msg` function, because we
332-
* are transforming away all calls to that function.
335+
* Return whether the given node is an import for the lit-localize module.
333336
*/
334-
removeMsgImport(
335-
imprt: ts.ImportDeclaration
336-
): ts.ImportDeclaration | undefined {
337-
const clause = imprt.importClause;
338-
if (clause === undefined) {
339-
return imprt;
337+
isLitLocalizeImport(node: ts.Node): node is ts.ImportDeclaration {
338+
if (!ts.isImportDeclaration(node)) {
339+
return false;
340340
}
341-
const bindings = clause.namedBindings;
342-
if (bindings === undefined || !ts.isNamedImports(bindings)) {
343-
return imprt;
341+
const moduleSymbol = this.typeChecker.getSymbolAtLocation(
342+
node.moduleSpecifier
343+
);
344+
if (!moduleSymbol) {
345+
return false;
344346
}
345-
// TODO(aomarks) This is too crude. We should do better to identify only our
346-
// `msg` function.
347-
if (
348-
bindings.elements.length === 1 &&
349-
bindings.elements[0].name.text === 'msg'
350-
) {
351-
return undefined;
347+
// TODO(aomarks) Is there a better way to reliably identify the lit-localize
348+
// module that doesn't require this cast? We could export a const with a
349+
// known name and then look through `exports`, but it doesn't seem good to
350+
// polute the module like that.
351+
const file = (moduleSymbol.valueDeclaration as unknown) as {
352+
identifiers: Map<string, unknown>;
353+
};
354+
for (const id of file.identifiers.keys()) {
355+
if (id === '_LIT_LOCALIZE_MSG_') {
356+
return true;
357+
}
352358
}
353-
return imprt;
359+
return false;
354360
}
355361
}
356362

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 isLitExpression(
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,

src/tests/transform.unit.test.ts

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,22 @@ function checkTransform(
2929
t: ExecutionContext,
3030
inputTs: string,
3131
expectedJs: string,
32-
messages: Message[]
32+
messages: Message[],
33+
autoImport = true
3334
) {
34-
// Rather than fuss with imports in all the test cases, this little hack
35-
// automatically imports for `msg` and `html` (assuming those strings aren't
36-
// used with any other meanings).
37-
if (inputTs.includes('msg')) {
38-
inputTs = IMPORT_MSG + inputTs;
39-
// Note we don't expect to see the `msg` import in the output JS, since it
40-
// should be un-used after litLocalizeTransformation.
41-
}
42-
if (inputTs.includes('html')) {
43-
inputTs = IMPORT_LIT_HTML + inputTs;
44-
expectedJs = IMPORT_LIT_HTML + expectedJs;
35+
if (autoImport) {
36+
// Rather than fuss with imports in all the test cases, this little hack
37+
// automatically imports for `msg` and `html` (assuming those strings aren't
38+
// used with any other meanings).
39+
if (inputTs.includes('msg')) {
40+
inputTs = IMPORT_MSG + inputTs;
41+
// Note we don't expect to see the `msg` import in the output JS, since it
42+
// should be un-used after litLocalizeTransformation.
43+
}
44+
if (inputTs.includes('html')) {
45+
inputTs = IMPORT_LIT_HTML + inputTs;
46+
expectedJs = IMPORT_LIT_HTML + expectedJs;
47+
}
4548
}
4649
const options = ts.getDefaultCompilerOptions();
4750
options.target = ts.ScriptTarget.ES2015;
@@ -50,9 +53,9 @@ function checkTransform(
5053
// Don't automatically load typings from nodes_modules/@types, we're not using
5154
// them here, so it's a waste of time.
5255
options.typeRoots = [];
53-
const result = compileTsFragment(inputTs, options, cache, {
54-
before: [litLocalizeTransform(makeMessageIdMap(messages))],
55-
});
56+
const result = compileTsFragment(inputTs, options, cache, (program) => ({
57+
before: [litLocalizeTransform(makeMessageIdMap(messages), program)],
58+
}));
5659

5760
let formattedExpected = prettier.format(expectedJs, {parser: 'typescript'});
5861
let formattedActual;
@@ -280,3 +283,41 @@ test('msg(fn(string), msg(string)) translated', (t) => {
280283
]
281284
);
282285
});
286+
287+
test('import * as litLocalize', (t) => {
288+
checkTransform(
289+
t,
290+
`
291+
import * as litLocalize from './lib_client/index.js';
292+
litLocalize.msg("foo", "Hello World");
293+
`,
294+
'"Hello World";',
295+
[],
296+
false
297+
);
298+
});
299+
300+
test('import {msg as foo}', (t) => {
301+
checkTransform(
302+
t,
303+
`
304+
import {msg as foo} from './lib_client/index.js';
305+
foo("foo", "Hello World");
306+
`,
307+
'"Hello World";',
308+
[],
309+
false
310+
);
311+
});
312+
313+
test('exclude different msg function', (t) => {
314+
checkTransform(
315+
t,
316+
`function msg(id: string, template: string) { return template; }
317+
msg("foo", "Hello World");`,
318+
`function msg(id, template) { return template; }
319+
msg("foo", "Hello World");`,
320+
[],
321+
false
322+
);
323+
});

src_client/index.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,23 +166,23 @@ export function localeReady(): Promise<void> {
166166
* @param args In the case that `template` is a function, it is invoked with
167167
* the 3rd and onwards arguments to `msg`.
168168
*/
169-
export function msg(id: string, template: string): string;
169+
export function _msg(id: string, template: string): string;
170170

171-
export function msg(id: string, template: TemplateResult): TemplateResult;
171+
export function _msg(id: string, template: TemplateResult): TemplateResult;
172172

173-
export function msg<F extends (...args: any[]) => string>(
173+
export function _msg<F extends (...args: any[]) => string>(
174174
id: string,
175175
fn: F,
176176
...params: Parameters<F>
177177
): string;
178178

179-
export function msg<F extends (...args: any[]) => TemplateResult>(
179+
export function _msg<F extends (...args: any[]) => TemplateResult>(
180180
id: string,
181181
fn: F,
182182
...params: Parameters<F>
183183
): TemplateResult;
184184

185-
export function msg(
185+
export function _msg(
186186
id: string,
187187
template: TemplateLike,
188188
...params: any[]
@@ -198,3 +198,5 @@ export function msg(
198198
}
199199
return template;
200200
}
201+
202+
export const msg: typeof _msg & {_LIT_LOCALIZE_MSG_?: never} = _msg;

0 commit comments

Comments
 (0)