1
1
import {
2
2
AstParser ,
3
- AstTraverser ,
4
3
ExtractedExport ,
5
4
ExtractedFunction ,
6
5
extractExports ,
7
6
extractFunctions ,
7
+ findFirst ,
8
+ findLiteral ,
9
+ findMemberCall ,
10
+ findRawLiteral ,
8
11
findTopLevelConstants ,
9
- guardBinaryExpression ,
10
12
guardCallExpression ,
11
13
guardIdentifier ,
12
14
guardLiteral ,
15
+ IdentifierWithName ,
13
16
ProgramConstant ,
17
+ SpecificFunctionCall ,
14
18
StructureError ,
15
19
traverse ,
16
20
} from '@exercism/static-analysis'
17
- import { TSESTree , AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'
21
+ import { AST_NODE_TYPES , TSESTree } from '@typescript-eslint/typescript-estree'
18
22
import { readFileSync } from 'fs'
23
+ import path from 'path'
19
24
import { assertNamedExport } from '~src/asserts/assert_named_export'
20
25
import { assertNamedFunction } from '~src/asserts/assert_named_function'
26
+ import { assertPublicApi } from '../../../asserts/assert_public_api'
27
+ import { assertPublicConstant } from '../../../asserts/assert_public_constant'
21
28
import { Source } from '../../SourceImpl'
22
29
import { parameterName } from '../../utils/extract_parameter'
23
- import path from 'path'
24
30
25
- const REMAINING_MINUTES_IN_OVEN = 'remainingMinutesInOven'
26
- const PREPARATION_TIME_IN_MINUTES = 'preparationTimeInMinutes'
27
- const TOTAL_TIME_IN_MINUTES = 'totalTimeInMinutes'
28
- const EXPECTED_MINUTES_IN_OVEN = 'EXPECTED_MINUTES_IN_OVEN'
31
+ export const REMAINING_MINUTES_IN_OVEN = 'remainingMinutesInOven'
32
+ export const PREPARATION_TIME_IN_MINUTES = 'preparationTimeInMinutes'
33
+ export const TOTAL_TIME_IN_MINUTES = 'totalTimeInMinutes'
34
+ export const EXPECTED_MINUTES_IN_OVEN = 'EXPECTED_MINUTES_IN_OVEN'
29
35
30
- export class NoPublicConstantError extends StructureError {
31
- constructor ( public name : string ) {
32
- super ( `Expected an exported constant named ${ name } , but did not find it.` )
36
+ class RemainingMinutesInOven {
37
+ public readonly parameter : string
33
38
34
- Error . captureStackTrace ( this , this . constructor )
39
+ constructor ( private readonly implementation : ExtractedFunction ) {
40
+ this . parameter = parameterName ( implementation . params [ 0 ] )
35
41
}
36
- }
37
42
38
- function assertPublicApi (
39
- exported : string ,
40
- exports : ExtractedExport [ ] ,
41
- functions : ExtractedFunction [ ]
42
- ) : ExtractedFunction {
43
- const namedExport = assertNamedExport ( exported , exports )
44
- return assertNamedFunction ( namedExport . local , functions )
45
- }
46
-
47
- function assertPublicConstant (
48
- exported : string ,
49
- exports : ExtractedExport [ ] ,
50
- root : TSESTree . Node
51
- ) : ProgramConstant {
52
- const namedExport = assertNamedExport ( exported , exports )
53
- const result = findTopLevelConstants ( root , [ 'let' , 'const' , 'var' ] ) . find (
54
- ( constant ) =>
55
- guardIdentifier ( constant . id ) && constant . id . name === namedExport . name
56
- )
57
-
58
- if ( ! result ) {
59
- throw new NoPublicConstantError ( exported )
43
+ public traverse ( options : Parameters < typeof traverse > [ 1 ] ) : void {
44
+ traverse ( this . implementation . body , options )
60
45
}
61
46
62
- return result
63
- }
64
-
65
- export class LasagnaSolution {
66
- private readonly source : Source
67
-
68
- private readonly remainingMinutesInOven : ExtractedFunction
69
- private readonly preparationTimeInMinutes : ExtractedFunction
70
- private readonly totalTimeInMinutes : ExtractedFunction
71
- private readonly expectedMinutesInOven : ProgramConstant
72
-
73
- private exemplar ! : Source
74
-
75
- constructor ( public readonly program : TSESTree . Program , source : string ) {
76
- this . source = new Source ( source )
77
-
78
- const functions = extractFunctions ( program )
79
- const exports = extractExports ( program )
80
-
81
- this . expectedMinutesInOven = assertPublicConstant (
82
- EXPECTED_MINUTES_IN_OVEN ,
83
- exports ,
84
- program
85
- )
86
-
87
- this . remainingMinutesInOven = assertPublicApi (
88
- REMAINING_MINUTES_IN_OVEN ,
89
- exports ,
90
- functions
91
- )
92
-
93
- this . preparationTimeInMinutes = assertPublicApi (
94
- PREPARATION_TIME_IN_MINUTES ,
95
- exports ,
96
- functions
97
- )
98
-
99
- this . totalTimeInMinutes = assertPublicApi (
100
- TOTAL_TIME_IN_MINUTES ,
101
- exports ,
102
- functions
103
- )
104
- }
105
-
106
- public readExemplar ( directory : string ) : void {
107
- const configPath = path . join ( directory , '.meta' , 'config.json' )
108
- const config = JSON . parse ( readFileSync ( configPath ) . toString ( ) )
109
-
110
- const exemplarPath = path . join ( directory , config . files . exemplar [ 0 ] )
111
- this . exemplar = new Source ( readFileSync ( exemplarPath ) . toString ( ) )
112
- }
113
-
114
- public get isExemplar ( ) : boolean {
115
- const sourceAst = AstParser . REPRESENTER . parseSync ( this . source . toString ( ) )
116
- const exemplarAst = AstParser . REPRESENTER . parseSync (
117
- this . exemplar . toString ( )
118
- )
119
-
120
- return (
121
- JSON . stringify ( sourceAst [ 0 ] . program ) ===
122
- JSON . stringify ( exemplarAst [ 0 ] . program )
123
- )
124
- }
125
-
126
- public get hasConstantDeclaredAsConst ( ) : boolean {
127
- return this . expectedMinutesInOven . kind === 'const'
128
- }
129
-
130
- public get hasOptimalConstant ( ) : boolean {
131
- return guardLiteral ( this . expectedMinutesInOven . init ! , 40 )
132
- }
133
-
134
- public get hasOptimalRemainingMinutesInOven ( ) : boolean {
135
- const parameter = parameterName ( this . remainingMinutesInOven . params [ 0 ] )
47
+ public get isOptimal ( ) : boolean {
48
+ const parameter = this . parameter
136
49
137
50
let foundSuboptimalNode = false
138
- traverse ( this . remainingMinutesInOven . body , {
51
+ this . traverse ( {
139
52
enter ( ) {
140
53
foundSuboptimalNode = true
141
54
} ,
@@ -170,11 +83,36 @@ export class LasagnaSolution {
170
83
return ! foundSuboptimalNode
171
84
}
172
85
173
- public get hasOptimalPreparationTimeInMinutes ( ) : boolean {
174
- const parameter = parameterName ( this . preparationTimeInMinutes . params [ 0 ] )
86
+ public get hasReplacableLiteral ( ) : boolean {
87
+ const hasConstantLiteral = findFirst (
88
+ this . implementation . body ,
89
+ ( node ) : node is IdentifierWithName < typeof EXPECTED_MINUTES_IN_OVEN > =>
90
+ guardIdentifier ( node , EXPECTED_MINUTES_IN_OVEN )
91
+ )
92
+ const hasLiteral = findRawLiteral ( this . implementation . body , '40' )
93
+ return Boolean ( hasLiteral && ! hasConstantLiteral )
94
+ }
95
+ }
96
+
97
+ class TotalTimeInMinutes {
98
+ public readonly numberOfLayers : string
99
+ public readonly actualMinutesInOven : string
100
+
101
+ constructor ( private readonly implementation : ExtractedFunction ) {
102
+ this . numberOfLayers = parameterName ( this . implementation . params [ 0 ] )
103
+ this . actualMinutesInOven = parameterName ( this . implementation . params [ 1 ] )
104
+ }
105
+
106
+ public traverse ( options : Parameters < typeof traverse > [ 1 ] ) : void {
107
+ traverse ( this . implementation . body , options )
108
+ }
109
+
110
+ public get isOptimal ( ) : boolean {
111
+ const numberOfLayers = this . numberOfLayers
112
+ const actualMinutesInOven = this . actualMinutesInOven
175
113
176
114
let foundSuboptimalNode = false
177
- traverse ( this . preparationTimeInMinutes . body , {
115
+ this . traverse ( {
178
116
enter ( ) {
179
117
foundSuboptimalNode = true
180
118
} ,
@@ -190,14 +128,17 @@ export class LasagnaSolution {
190
128
} ,
191
129
192
130
[ AST_NODE_TYPES . BinaryExpression ] ( node ) {
193
- // TODO look for top-level constant and use that name instead.
194
131
foundSuboptimalNode =
195
- node . operator !== '* ' ||
132
+ node . operator !== '+ ' ||
196
133
! (
197
- ( guardIdentifier ( node . left , parameter ) &&
198
- guardIdentifier ( node . right , 'PREPARATION_MINUTES_PER_LAYER' ) ) ||
199
- ( guardIdentifier ( node . right , parameter ) &&
200
- guardIdentifier ( node . left , 'PREPARATION_MINUTES_PER_LAYER' ) )
134
+ ( guardCallExpression ( node . left , PREPARATION_TIME_IN_MINUTES ) &&
135
+ node . left . arguments . length === 1 &&
136
+ guardIdentifier ( node . left . arguments [ 0 ] , numberOfLayers ) &&
137
+ guardIdentifier ( node . right , actualMinutesInOven ) ) ||
138
+ ( guardCallExpression ( node . right , PREPARATION_TIME_IN_MINUTES ) &&
139
+ node . right . arguments . length === 1 &&
140
+ guardIdentifier ( node . right . arguments [ 0 ] , numberOfLayers ) &&
141
+ guardIdentifier ( node . left , actualMinutesInOven ) )
201
142
)
202
143
this . skip ( )
203
144
} ,
@@ -212,12 +153,91 @@ export class LasagnaSolution {
212
153
return ! foundSuboptimalNode
213
154
}
214
155
215
- public get hasOptimalTotalTimeInMinutes ( ) : boolean {
216
- const numberOfLayers = parameterName ( this . totalTimeInMinutes . params [ 0 ] )
217
- const actualMinutesInOven = parameterName ( this . totalTimeInMinutes . params [ 1 ] )
156
+ public get hasCallToPreparationTime ( ) : boolean {
157
+ return ! ! findFirst (
158
+ this . implementation . body ,
159
+ (
160
+ node
161
+ ) : node is SpecificFunctionCall < typeof PREPARATION_TIME_IN_MINUTES > =>
162
+ guardCallExpression ( node , PREPARATION_TIME_IN_MINUTES )
163
+ )
164
+ }
165
+ }
166
+
167
+ export class LasagnaSolution {
168
+ private readonly source : Source
169
+
170
+ public readonly remainingMinutesInOven : RemainingMinutesInOven
171
+ public readonly totalTimeInMinutes : TotalTimeInMinutes
172
+ private readonly preparationTimeInMinutes : ExtractedFunction
173
+ private readonly expectedMinutesInOven : ProgramConstant
174
+
175
+ private exemplar ! : Source
176
+
177
+ constructor ( public readonly program : TSESTree . Program , source : string ) {
178
+ this . source = new Source ( source )
179
+
180
+ const functions = extractFunctions ( program )
181
+ const exports = extractExports ( program )
182
+
183
+ this . expectedMinutesInOven = assertPublicConstant (
184
+ EXPECTED_MINUTES_IN_OVEN ,
185
+ exports ,
186
+ program
187
+ )
188
+
189
+ this . preparationTimeInMinutes = assertPublicApi (
190
+ PREPARATION_TIME_IN_MINUTES ,
191
+ exports ,
192
+ functions
193
+ )
194
+
195
+ this . remainingMinutesInOven = new RemainingMinutesInOven (
196
+ assertPublicApi ( REMAINING_MINUTES_IN_OVEN , exports , functions )
197
+ )
198
+
199
+ this . totalTimeInMinutes = new TotalTimeInMinutes (
200
+ assertPublicApi ( TOTAL_TIME_IN_MINUTES , exports , functions )
201
+ )
202
+ }
203
+
204
+ public readExemplar ( directory : string ) : void {
205
+ const configPath = path . join ( directory , '.meta' , 'config.json' )
206
+ const config = JSON . parse ( readFileSync ( configPath ) . toString ( ) )
207
+
208
+ const exemplarPath = path . join ( directory , config . files . exemplar [ 0 ] )
209
+ this . exemplar = new Source ( readFileSync ( exemplarPath ) . toString ( ) )
210
+ }
211
+
212
+ public get isExemplar ( ) : boolean {
213
+ const sourceAst = AstParser . REPRESENTER . parseSync ( this . source . toString ( ) )
214
+ const exemplarAst = AstParser . REPRESENTER . parseSync (
215
+ this . exemplar . toString ( )
216
+ )
217
+
218
+ return (
219
+ JSON . stringify ( sourceAst [ 0 ] . program ) ===
220
+ JSON . stringify ( exemplarAst [ 0 ] . program )
221
+ )
222
+ }
223
+
224
+ public get constantKind ( ) : ProgramConstant [ 'kind' ] {
225
+ return this . expectedMinutesInOven . kind
226
+ }
227
+
228
+ public get hasConstantDeclaredAsConst ( ) : boolean {
229
+ return this . constantKind === 'const'
230
+ }
231
+
232
+ public get hasOptimalConstant ( ) : boolean {
233
+ return guardLiteral ( this . expectedMinutesInOven . init ! , 40 )
234
+ }
235
+
236
+ public get hasOptimalPreparationTimeInMinutes ( ) : boolean {
237
+ const parameter = parameterName ( this . preparationTimeInMinutes . params [ 0 ] )
218
238
219
239
let foundSuboptimalNode = false
220
- traverse ( this . totalTimeInMinutes . body , {
240
+ traverse ( this . preparationTimeInMinutes . body , {
221
241
enter ( ) {
222
242
foundSuboptimalNode = true
223
243
} ,
@@ -233,17 +253,14 @@ export class LasagnaSolution {
233
253
} ,
234
254
235
255
[ AST_NODE_TYPES . BinaryExpression ] ( node ) {
256
+ // TODO look for top-level constant and use that name instead.
236
257
foundSuboptimalNode =
237
- node . operator !== '+ ' ||
258
+ node . operator !== '* ' ||
238
259
! (
239
- ( guardCallExpression ( node . left , PREPARATION_TIME_IN_MINUTES ) &&
240
- node . left . arguments . length === 1 &&
241
- guardIdentifier ( node . left . arguments [ 0 ] , numberOfLayers ) &&
242
- guardIdentifier ( node . right , actualMinutesInOven ) ) ||
243
- ( guardCallExpression ( node . right , PREPARATION_TIME_IN_MINUTES ) &&
244
- node . right . arguments . length === 1 &&
245
- guardIdentifier ( node . right . arguments [ 0 ] , numberOfLayers ) &&
246
- guardIdentifier ( node . left , actualMinutesInOven ) )
260
+ ( guardIdentifier ( node . left , parameter ) &&
261
+ guardIdentifier ( node . right , 'PREPARATION_MINUTES_PER_LAYER' ) ) ||
262
+ ( guardIdentifier ( node . right , parameter ) &&
263
+ guardIdentifier ( node . left , 'PREPARATION_MINUTES_PER_LAYER' ) )
247
264
)
248
265
this . skip ( )
249
266
} ,
0 commit comments