Skip to content

Commit 20c8326

Browse files
committed
Add commandline --build parsing
1 parent 8ab3003 commit 20c8326

File tree

93 files changed

+779
-225
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

93 files changed

+779
-225
lines changed

internal/core/buildoptions.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package core
2+
3+
type BuildOptions struct {
4+
_ noCopy
5+
6+
Dry Tristate `json:"dry,omitzero"`
7+
Force Tristate `json:"force,omitzero"`
8+
Verbose Tristate `json:"verbose,omitzero"`
9+
StopBuildOnErrors Tristate `json:"stopBuildOnErrors,omitzero"`
10+
11+
// CompilerOptions are not parsed here and will be available on ParsedBuildCommandLine
12+
13+
// Internal fields
14+
Clean Tristate `json:"clean,omitzero"`
15+
}

internal/core/compileroptions.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ type CompilerOptions struct {
2626
AllowUnusedLabels Tristate `json:"allowUnusedLabels,omitzero"`
2727
AssumeChangesOnlyAffectDirectDependencies Tristate `json:"assumeChangesOnlyAffectDirectDependencies,omitzero"`
2828
AlwaysStrict Tristate `json:"alwaysStrict,omitzero"`
29-
Build Tristate `json:"build,omitzero"`
3029
CheckJs Tristate `json:"checkJs,omitzero"`
3130
CustomConditions []string `json:"customConditions,omitzero"`
3231
Composite Tristate `json:"composite,omitzero"`
@@ -142,7 +141,7 @@ type CompilerOptions struct {
142141
Version Tristate `json:"version,omitzero"`
143142
Watch Tristate `json:"watch,omitzero"`
144143
ShowConfig Tristate `json:"showConfig,omitzero"`
145-
TscBuild Tristate `json:"tscBuild,omitzero"`
144+
Build Tristate `json:"build,omitzero"`
146145
Help Tristate `json:"help,omitzero"`
147146
All Tristate `json:"all,omitzero"`
148147

internal/core/projectreference.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ type ProjectReference struct {
99
}
1010

1111
func ResolveProjectReferencePath(ref *ProjectReference) string {
12-
return resolveConfigFileNameOfProjectReference(ref.Path)
12+
return ResolveConfigFileNameOfProjectReference(ref.Path)
1313
}
1414

15-
func resolveConfigFileNameOfProjectReference(path string) string {
15+
func ResolveConfigFileNameOfProjectReference(path string) string {
1616
if tspath.FileExtensionIs(path, tspath.ExtensionJson) {
1717
return path
1818
}

internal/outputpaths/outputpaths.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ func getDeclarationEmitExtensionForPath(fileName string) string {
195195
}
196196

197197
func GetBuildInfoFileName(options *core.CompilerOptions, opts tspath.ComparePathsOptions) string {
198-
if !options.IsIncremental() && !options.TscBuild.IsTrue() {
198+
if !options.IsIncremental() && !options.Build.IsTrue() {
199199
return ""
200200
}
201201
if options.TsBuildInfoFile != "" {

internal/tsoptions/commandlineparser.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,56 @@ func ParseCommandLine(
6767
}
6868
}
6969

70+
func ParseBuildCommandLine(
71+
commandLine []string,
72+
host ParseConfigHost,
73+
) *ParsedBuildCommandLine {
74+
if commandLine == nil {
75+
commandLine = []string{}
76+
}
77+
parser := parseCommandLineWorker(buildOptionsDidYouMeanDiagnostics, commandLine, host.FS())
78+
compilerOptions := &core.CompilerOptions{}
79+
for key, value := range parser.options.Entries() {
80+
buildOption := BuildNameMap.Get(key)
81+
if buildOption == &TscBuildOption || buildOption == CompilerNameMap.Get(key) {
82+
ParseCompilerOptions(key, value, compilerOptions)
83+
}
84+
}
85+
result := &ParsedBuildCommandLine{
86+
BuildOptions: convertMapToOptions(parser.options, &buildOptionsParser{&core.BuildOptions{}}).BuildOptions,
87+
CompilerOptions: compilerOptions,
88+
WatchOptions: convertMapToOptions(parser.options, &watchOptionsParser{&core.WatchOptions{}}).WatchOptions,
89+
Projects: parser.fileNames,
90+
Errors: parser.errors,
91+
92+
comparePathsOptions: tspath.ComparePathsOptions{
93+
UseCaseSensitiveFileNames: host.FS().UseCaseSensitiveFileNames(),
94+
CurrentDirectory: host.GetCurrentDirectory(),
95+
},
96+
}
97+
98+
if len(result.Projects) == 0 {
99+
// tsc -b invoked with no extra arguments; act as if invoked with "tsc -b ."
100+
result.Projects = append(result.Projects, ".")
101+
}
102+
103+
// Nonsensical combinations
104+
if result.BuildOptions.Clean.IsTrue() && result.BuildOptions.Force.IsTrue() {
105+
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "force"))
106+
}
107+
if result.BuildOptions.Clean.IsTrue() && result.BuildOptions.Verbose.IsTrue() {
108+
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "verbose"))
109+
}
110+
if result.BuildOptions.Clean.IsTrue() && result.CompilerOptions.Watch.IsTrue() {
111+
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "clean", "watch"))
112+
}
113+
if result.CompilerOptions.Watch.IsTrue() && result.BuildOptions.Dry.IsTrue() {
114+
result.Errors = append(result.Errors, ast.NewCompilerDiagnostic(diagnostics.Options_0_and_1_cannot_be_combined, "watch", "dry"))
115+
}
116+
117+
return result
118+
}
119+
70120
func parseCommandLineWorker(
71121
parseCommandLineWithDiagnostics *ParseCommandLineWorkerDiagnostics,
72122
commandLine []string,
@@ -116,7 +166,7 @@ func (p *commandLineParser) parseStrings(args []string) {
116166

117167
func getInputOptionName(input string) string {
118168
// removes at most two leading '-' from the input string
119-
return strings.ToLower(strings.TrimPrefix(strings.TrimPrefix(input, "-"), "-"))
169+
return strings.TrimPrefix(strings.TrimPrefix(input, "-"), "-")
120170
}
121171

122172
func (p *commandLineParser) parseResponseFile(fileName string) {

internal/tsoptions/commandlineparser_test.go

Lines changed: 154 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"github.com/microsoft/typescript-go/internal/testutil/baseline"
1616
"github.com/microsoft/typescript-go/internal/testutil/filefixture"
1717
"github.com/microsoft/typescript-go/internal/tsoptions"
18+
"github.com/microsoft/typescript-go/internal/tsoptions/tsoptionstest"
19+
"github.com/microsoft/typescript-go/internal/tspath"
1820
"github.com/microsoft/typescript-go/internal/vfs/osvfs"
1921
"gotest.tools/v3/assert"
2022
)
@@ -80,7 +82,7 @@ func TestCommandLineParseResult(t *testing.T) {
8082
}
8183

8284
for _, testCase := range parseCommandLineSubScenarios {
83-
testCase.createSubScenario().assertParseResult(t)
85+
testCase.createSubScenario("parseCommandLine").assertParseResult(t)
8486
}
8587
}
8688

@@ -89,7 +91,7 @@ func TestParseCommandLineVerifyNull(t *testing.T) {
8991
repo.SkipIfNoTypeScriptSubmodule(t)
9092

9193
// run test for boolean
92-
subScenarioInput{"allows setting option type boolean to false", []string{"--composite", "false", "0.ts"}}.createSubScenario().assertParseResult(t)
94+
subScenarioInput{"allows setting option type boolean to false", []string{"--composite", "false", "0.ts"}}.createSubScenario("parseCommandLine").assertParseResult(t)
9395

9496
verifyNullSubScenarios := []verifyNull{
9597
{
@@ -114,26 +116,30 @@ func TestParseCommandLineVerifyNull(t *testing.T) {
114116

115117
for _, verifyNullCase := range verifyNullSubScenarios {
116118
createSubScenario(
119+
"parseCommandLine",
117120
verifyNullCase.subScenario+" allows setting it to null",
118121
[]string{"--" + verifyNullCase.optionName, "null", "0.ts"},
119122
verifyNullCase.optDecls,
120123
).assertParseResult(t)
121124

122125
if verifyNullCase.nonNullValue != "" {
123126
createSubScenario(
127+
"parseCommandLine",
124128
verifyNullCase.subScenario+" errors if non null value is passed",
125129
[]string{"--" + verifyNullCase.optionName, verifyNullCase.nonNullValue, "0.ts"},
126130
verifyNullCase.optDecls,
127131
).assertParseResult(t)
128132
}
129133

130134
createSubScenario(
135+
"parseCommandLine",
131136
verifyNullCase.subScenario+" errors if its followed by another option",
132137
[]string{"0.ts", "--strictNullChecks", "--" + verifyNullCase.optionName},
133138
verifyNullCase.optDecls,
134139
).assertParseResult(t)
135140

136141
createSubScenario(
142+
"parseCommandLine",
137143
verifyNullCase.subScenario+" errors if its last option",
138144
[]string{"0.ts", "--" + verifyNullCase.optionName},
139145
verifyNullCase.optDecls,
@@ -195,10 +201,6 @@ func (f commandLineSubScenario) assertParseResult(t *testing.T) {
195201
})
196202
}
197203

198-
func (f *commandLineSubScenario) getBaselineName() (baseline.Options, string) {
199-
return baseline.Options{Subfolder: "tsoptions/commandLineParsing"}, f.testName
200-
}
201-
202204
func parseExistingCompilerBaseline(t *testing.T, baseline string) *TestCommandLineParser {
203205
_, rest, _ := strings.Cut(baseline, "CompilerOptions::\n")
204206
compilerOptions, rest, watchFound := strings.Cut(rest, "\nWatchOptions::\n")
@@ -243,27 +245,112 @@ func formatNewBaseline(
243245
return formatted.String()
244246
}
245247

246-
// todo: --build not implemented
247-
// func parseExistingBuildBaseline(baseline string) *TestCommandLineParser {
248-
// _, rest, _ := strings.Cut(baseline, "BuildOptions::\n")
249-
// buildOptions, rest, _ := strings.Cut(rest, "\nWatchOptions::\n")
250-
// _, rest, _ = strings.Cut(rest, "\nProjects::\n")
251-
// fileNames, errors, _ := strings.Cut(rest, "\nErrors::\n")
248+
func (f commandLineSubScenario) assertBuildParseResult(t *testing.T) {
249+
t.Helper()
250+
t.Run(f.testName, func(t *testing.T) {
251+
t.Parallel()
252+
originalBaseline := f.baseline.ReadFile(t)
253+
tsBaseline := parseExistingCompilerBaselineBuild(t, originalBaseline)
254+
255+
// f.workerDiagnostic is either defined or set to default pointer in `createSubScenario`
256+
parsed := tsoptions.ParseBuildCommandLine(f.commandLine, &tsoptionstest.VfsParseConfigHost{
257+
Vfs: osvfs.FS(),
258+
CurrentDirectory: tspath.NormalizeSlashes(repo.TypeScriptSubmodulePath),
259+
})
260+
261+
newBaselineProjects := strings.Join(parsed.Projects, ",")
262+
assert.Equal(t, tsBaseline.projects, newBaselineProjects)
263+
264+
o, _ := json.Marshal(parsed.BuildOptions)
265+
newParsedBuildOptions := &core.BuildOptions{}
266+
e := json.Unmarshal(o, newParsedBuildOptions)
267+
assert.NilError(t, e)
268+
assert.DeepEqual(t, tsBaseline.options, newParsedBuildOptions, cmpopts.IgnoreUnexported(core.BuildOptions{}))
269+
270+
compilerOpts, _ := json.Marshal(parsed.CompilerOptions)
271+
newParsedCompilerOptions := &core.CompilerOptions{}
272+
e = json.Unmarshal(compilerOpts, newParsedCompilerOptions)
273+
assert.NilError(t, e)
274+
assert.DeepEqual(t, tsBaseline.compilerOptions, newParsedCompilerOptions, cmpopts.IgnoreUnexported(core.CompilerOptions{}))
275+
276+
newParsedWatchOptions := core.WatchOptions{}
277+
e = json.Unmarshal(o, &newParsedWatchOptions)
278+
assert.NilError(t, e)
279+
280+
// !!! useful for debugging but will not pass due to `none` as enum options
281+
// assert.DeepEqual(t, tsBaseline.watchoptions, newParsedWatchOptions)
282+
283+
var formattedErrors strings.Builder
284+
diagnosticwriter.WriteFormatDiagnostics(&formattedErrors, parsed.Errors, &diagnosticwriter.FormattingOptions{NewLine: "\n"})
285+
newBaselineErrors := formattedErrors.String()
286+
287+
// !!!
288+
// useful for debugging--compares the new errors with the old errors. currently will NOT pass because of unimplemented options, not completely identical enum options, etc
289+
// assert.Equal(t, tsBaseline.errors, newBaselineErrors)
290+
291+
baseline.Run(t, f.testName+".js", formatNewBaselineBuild(f.commandLine, o, compilerOpts, newBaselineProjects, newBaselineErrors), baseline.Options{Subfolder: "tsoptions/commandLineParsing"})
292+
})
293+
}
294+
295+
func parseExistingCompilerBaselineBuild(t *testing.T, baseline string) *TestCommandLineParserBuild {
296+
_, rest, _ := strings.Cut(baseline, "buildOptions::\n")
297+
buildOptions, rest, watchFound := strings.Cut(rest, "\nWatchOptions::\n")
298+
watchOptions, rest, _ := strings.Cut(rest, "\nProjects::\n")
299+
projects, errors, _ := strings.Cut(rest, "\nErrors::\n")
300+
301+
baselineBuildOptions := &core.BuildOptions{}
302+
e := json.Unmarshal([]byte(buildOptions), &baselineBuildOptions)
303+
assert.NilError(t, e)
304+
305+
baselineCompilerOptions := &core.CompilerOptions{}
306+
e = json.Unmarshal([]byte(buildOptions), &baselineCompilerOptions)
307+
assert.NilError(t, e)
252308

253-
// // todo: change CompilerOptions to buildoptions
254-
// baselineOptions := &core.CompilerOptions{}
255-
// json.Unmarshal([]byte(buildOptions), &baselineOptions)
309+
baselineWatchOptions := &core.WatchOptions{}
310+
if watchFound && watchOptions != "" {
311+
e2 := json.Unmarshal([]byte(watchOptions), &baselineWatchOptions)
312+
assert.NilError(t, e2)
313+
}
314+
315+
return &TestCommandLineParserBuild{
316+
options: baselineBuildOptions,
317+
compilerOptions: baselineCompilerOptions,
318+
watchoptions: baselineWatchOptions,
319+
projects: projects,
320+
errors: errors,
321+
}
322+
}
256323

257-
// var parser = TestCommandLineParser{
258-
// options: *baselineOptions,
259-
// fileNames: fileNames,
260-
// errors: errors,
261-
// }
262-
// return &parser
263-
// }
324+
func formatNewBaselineBuild(
325+
commandLine []string,
326+
opts []byte,
327+
compilerOpts []byte,
328+
projects string,
329+
errors string,
330+
) string {
331+
var formatted strings.Builder
332+
formatted.WriteString("Args::\n")
333+
if len(commandLine) == 0 {
334+
formatted.WriteString("[]")
335+
} else {
336+
formatted.WriteString("[\"" + strings.Join(commandLine, "\", \"") + "\"]")
337+
}
338+
formatted.WriteString("\n\nbuildOptions::\n")
339+
formatted.Write(opts)
340+
formatted.WriteString("\n\ncompilerOptions::\n")
341+
formatted.Write(compilerOpts)
342+
// todo: watch options not implemented
343+
// formatted.WriteString("WatchOptions::\n")
344+
formatted.WriteString("\n\nProjects::\n")
345+
formatted.WriteString(projects)
346+
formatted.WriteString("\n\nErrors::\n")
347+
formatted.WriteString(errors)
348+
return formatted.String()
349+
}
264350

265-
func createSubScenario(subScenarioName string, commandline []string, opts ...[]*tsoptions.CommandLineOption) *commandLineSubScenario {
266-
baselineFileName := "tests/baselines/reference/config/commandLineParsing/parseCommandLine/" + subScenarioName + ".js"
351+
func createSubScenario(scenarioKind string, subScenarioName string, commandline []string, opts ...[]*tsoptions.CommandLineOption) *commandLineSubScenario {
352+
subScenarioName = scenarioKind + "/" + subScenarioName
353+
baselineFileName := "tests/baselines/reference/config/commandLineParsing/" + subScenarioName + ".js"
267354

268355
result := &commandLineSubScenario{
269356
filefixture.FromFile(subScenarioName, filepath.Join(repo.TypeScriptSubmodulePath, baselineFileName)),
@@ -282,8 +369,8 @@ type subScenarioInput struct {
282369
commandLineArgs []string
283370
}
284371

285-
func (f subScenarioInput) createSubScenario() *commandLineSubScenario {
286-
return createSubScenario(f.name, f.commandLineArgs)
372+
func (f subScenarioInput) createSubScenario(scenarioKind string) *commandLineSubScenario {
373+
return createSubScenario(scenarioKind, f.name, f.commandLineArgs)
287374
}
288375

289376
type commandLineSubScenario struct {
@@ -306,6 +393,47 @@ type TestCommandLineParser struct {
306393
fileNames, errors string
307394
}
308395

396+
type TestCommandLineParserBuild struct {
397+
options *core.BuildOptions
398+
compilerOptions *core.CompilerOptions
399+
watchoptions *core.WatchOptions
400+
projects, errors string
401+
}
402+
403+
func TestParseBuildCommandLine(t *testing.T) {
404+
t.Parallel()
405+
repo.SkipIfNoTypeScriptSubmodule(t)
406+
407+
parseCommandLineSubScenarios := []*subScenarioInput{
408+
{"parse build without any options ", []string{}},
409+
{"Parse multiple options", []string{"--verbose", "--force", "tests"}},
410+
{"Parse option with invalid option", []string{"--verbose", "--invalidOption"}},
411+
{"Parse multiple flags with input projects at the end", []string{"--force", "--verbose", "src", "tests"}},
412+
{"Parse multiple flags with input projects in the middle", []string{"--force", "src", "tests", "--verbose"}},
413+
{"Parse multiple flags with input projects in the beginning", []string{"src", "tests", "--force", "--verbose"}},
414+
{"parse build with --incremental", []string{"--incremental", "tests"}},
415+
{"parse build with --locale en-us", []string{"--locale", "en-us", "src"}},
416+
{"parse build with --tsBuildInfoFile", []string{"--tsBuildInfoFile", "build.tsbuildinfo", "tests"}},
417+
{"reports other common may not be used with --build flags", []string{"--strict"}},
418+
{`--clean and --force together is invalid`, []string{"--clean", "--force"}},
419+
{`--clean and --verbose together is invalid`, []string{"--clean", "--verbose"}},
420+
{`--clean and --watch together is invalid`, []string{"--clean", "--watch"}},
421+
{`--watch and --dry together is invalid`, []string{"--watch", "--dry"}},
422+
{"parse --watchFile", []string{"--watchFile", "UseFsEvents", "--verbose"}},
423+
{"parse --watchDirectory", []string{"--watchDirectory", "FixedPollingInterval", "--verbose"}},
424+
{"parse --fallbackPolling", []string{"--fallbackPolling", "PriorityInterval", "--verbose"}},
425+
{"parse --synchronousWatchDirectory", []string{"--synchronousWatchDirectory", "--verbose"}},
426+
{"errors on missing argument", []string{"--verbose", "--fallbackPolling"}},
427+
{"errors on invalid excludeDirectories", []string{"--excludeDirectories", "**/../*"}},
428+
{"parse --excludeFiles", []string{"--excludeFiles", "**/temp/*.ts"}},
429+
{"errors on invalid excludeFiles", []string{"--excludeFiles", "**/../*"}},
430+
}
431+
432+
for _, testCase := range parseCommandLineSubScenarios {
433+
testCase.createSubScenario("parseBuildOptions").assertBuildParseResult(t)
434+
}
435+
}
436+
309437
func TestAffectsBuildInfo(t *testing.T) {
310438
t.Parallel()
311439
t.Run("should have affectsBuildInfo true for every option with affectsSemanticDiagnostics", func(t *testing.T) {

internal/tsoptions/decls_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ func TestCompilerOptionsDeclaration(t *testing.T) {
2626
"noEmitForJsFiles",
2727
"pathsBasePath",
2828
"suppressOutputPathCheck",
29-
"tscBuild",
29+
"build",
3030
}
3131

3232
internalOptionsMap := make(map[string]string)

0 commit comments

Comments
 (0)