modelbuilder: add `try_get_mclass_by_qid` to handle qualified class names
[nit.git] / src / modelbuilder_base.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2012 Jean Privat <jean@pryen.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Load nit source files and build the associated model
18 #
19 # FIXME better doc
20 #
21 # FIXME split this module into submodules
22 # FIXME add missing error checks
23 module modelbuilder_base
24
25 import model
26 import toolcontext
27 import parser
28
29 private import more_collections
30
31 ###
32
33 redef class ToolContext
34
35 # The modelbuilder 1-to-1 associated with the toolcontext
36 fun modelbuilder: ModelBuilder do return modelbuilder_real.as(not null)
37
38 private var modelbuilder_real: nullable ModelBuilder = null
39
40 end
41
42 # A model builder knows how to load nit source files and build the associated model
43 class ModelBuilder
44 # The model where new modules, classes and properties are added
45 var model: Model
46
47 # The toolcontext used to control the interaction with the user (getting options and displaying messages)
48 var toolcontext: ToolContext
49
50 # Instantiate a modelbuilder for a model and a toolcontext
51 # Important, the options of the toolcontext must be correctly set (parse_option already called)
52 init
53 do
54 assert toolcontext.modelbuilder_real == null
55 toolcontext.modelbuilder_real = self
56 end
57
58 # Return a class named `name` visible by the module `mmodule`.
59 # Visibility in modules is correctly handled.
60 # If no such a class exists, then null is returned.
61 # If more than one class exists, then an error on `anode` is displayed and null is returned.
62 # FIXME: add a way to handle class name conflict
63 fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
64 do
65 var classes = model.get_mclasses_by_name(name)
66 if classes == null then
67 return null
68 end
69
70 var res: nullable MClass = null
71 for mclass in classes do
72 if not mmodule.in_importation <= mclass.intro_mmodule then continue
73 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
74 if res == null then
75 res = mclass
76 else
77 error(anode, "Error: ambiguous class name `{name}`; conflict between `{mclass.full_name}` and `{res.full_name}`.")
78 return null
79 end
80 end
81 return res
82 end
83
84 # Return a class identified by `qid` visible by the module `mmodule`.
85 # Visibility in modules and qualified names are correctly handled.
86 #
87 # If more than one class exists, then null is silently returned.
88 # It is up to the caller to post-analysis the result and display a correct error message.
89 fun try_get_mclass_by_qid(qid: AQclassid, mmodule: MModule): nullable MClass
90 do
91 var name = qid.n_id.text
92
93 var classes = model.get_mclasses_by_name(name)
94 if classes == null then
95 return null
96 end
97
98 var res: nullable MClass = null
99 for mclass in classes do
100 if not mmodule.in_importation <= mclass.intro_mmodule then continue
101 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
102 if not qid.accept(mclass) then continue
103 if res == null then
104 res = mclass
105 else
106 return null
107 end
108 end
109
110 return res
111 end
112
113 # Like `try_get_mclass_by_name` but display an error message when the class is not found
114 fun get_mclass_by_name(node: ANode, mmodule: MModule, name: String): nullable MClass
115 do
116 var mclass = try_get_mclass_by_name(node, mmodule, name)
117 if mclass == null then
118 error(node, "Type Error: missing primitive class `{name}'.")
119 end
120 return mclass
121 end
122
123 # Return a property named `name` on the type `mtype` visible in the module `mmodule`.
124 # Visibility in modules is correctly handled.
125 # Protected properties are returned (it is up to the caller to check and reject protected properties).
126 # If no such a property exists, then null is returned.
127 # If more than one property exists, then an error on `anode` is displayed and null is returned.
128 # FIXME: add a way to handle property name conflict
129 fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
130 do
131 var props = self.model.get_mproperties_by_name(name)
132 if props == null then
133 return null
134 end
135
136 var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
137 if cache != null then return cache
138
139 var res: nullable MProperty = null
140 var ress: nullable Array[MProperty] = null
141 for mprop in props do
142 if not mtype.has_mproperty(mmodule, mprop) then continue
143 if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue
144
145 # new-factories are invisible outside of the class
146 if mprop isa MMethod and mprop.is_new and (not mtype isa MClassType or mprop.intro_mclassdef.mclass != mtype.mclass) then
147 continue
148 end
149
150 if res == null then
151 res = mprop
152 continue
153 end
154
155 # Two global properties?
156 # First, special case for init, keep the most specific ones
157 if res isa MMethod and mprop isa MMethod and res.is_init and mprop.is_init then
158 var restype = res.intro_mclassdef.bound_mtype
159 var mproptype = mprop.intro_mclassdef.bound_mtype
160 if mproptype.is_subtype(mmodule, null, restype) then
161 # found a most specific constructor, so keep it
162 res = mprop
163 continue
164 end
165 end
166
167 # Ok, just keep all prop in the ress table
168 if ress == null then
169 ress = new Array[MProperty]
170 ress.add(res)
171 end
172 ress.add(mprop)
173 end
174
175 # There is conflict?
176 if ress != null and res isa MMethod and res.is_init then
177 # special case forinit again
178 var restype = res.intro_mclassdef.bound_mtype
179 var ress2 = new Array[MProperty]
180 for mprop in ress do
181 var mproptype = mprop.intro_mclassdef.bound_mtype
182 if not restype.is_subtype(mmodule, null, mproptype) then
183 ress2.add(mprop)
184 else if not mprop isa MMethod or not mprop.is_init then
185 ress2.add(mprop)
186 end
187 end
188 if ress2.is_empty then
189 ress = null
190 else
191 ress = ress2
192 ress.add(res)
193 end
194 end
195
196 if ress != null then
197 assert ress.length > 1
198 var s = new Array[String]
199 for mprop in ress do s.add mprop.full_name
200 self.error(anode, "Error: ambiguous property name `{name}` for `{mtype}`; conflict between {s.join(" and ")}.")
201 end
202
203 self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
204 return res
205 end
206
207 private var try_get_mproperty_by_name2_cache = new HashMap3[MModule, MType, String, nullable MProperty]
208
209
210 # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
211 fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
212 do
213 return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
214 end
215
216 # Helper function to display an error on a node.
217 # Alias for `self.toolcontext.error(n.hot_location, text)`
218 #
219 # This automatically sets `n.is_broken` to true.
220 fun error(n: nullable ANode, text: String)
221 do
222 var l = null
223 if n != null then
224 l = n.hot_location
225 n.is_broken = true
226 end
227 self.toolcontext.error(l, text)
228 end
229
230 # Helper function to display a warning on a node.
231 # Alias for: `self.toolcontext.warning(n.hot_location, text)`
232 fun warning(n: nullable ANode, tag, text: String)
233 do
234 var l = null
235 if n != null then l = n.hot_location
236 self.toolcontext.warning(l, tag, text)
237 end
238
239 # Helper function to display an advice on a node.
240 # Alias for: `self.toolcontext.advice(n.hot_location, text)`
241 fun advice(n: nullable ANode, tag, text: String)
242 do
243 var l = null
244 if n != null then l = n.hot_location
245 self.toolcontext.advice(l, tag, text)
246 end
247
248 # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
249 fun force_get_primitive_method(n: nullable ANode, name: String, recv: MClass, mmodule: MModule): MMethod
250 do
251 var res = mmodule.try_get_primitive_method(name, recv)
252 if res == null then
253 var l = null
254 if n != null then l = n.hot_location
255 self.toolcontext.fatal_error(l, "Fatal Error: `{recv}` must have a property named `{name}`.")
256 abort
257 end
258 return res
259 end
260
261 # Return the static type associated to the node `ntype`.
262 # `mmodule` and `mclassdef` is the context where the call is made (used to understand formal types)
263 # In case of problem, an error is displayed on `ntype` and null is returned.
264 # FIXME: the name "resolve_mtype" is awful
265 fun resolve_mtype_unchecked(mmodule: MModule, mclassdef: nullable MClassDef, ntype: AType, with_virtual: Bool): nullable MType
266 do
267 var qid = ntype.n_qid
268 var name = qid.n_id.text
269 var res: MType
270
271 # Check virtual type
272 if mclassdef != null and with_virtual then
273 var prop = try_get_mproperty_by_name(ntype, mclassdef, name).as(nullable MVirtualTypeProp)
274 if prop != null then
275 if not ntype.n_types.is_empty then
276 error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
277 end
278 res = prop.mvirtualtype
279 if ntype.n_kwnullable != null then res = res.as_nullable
280 ntype.mtype = res
281 return res
282 end
283 end
284
285 # Check parameter type
286 if mclassdef != null then
287 for p in mclassdef.mclass.mparameters do
288 if p.name != name then continue
289
290 if not ntype.n_types.is_empty then
291 error(ntype, "Type Error: formal type `{name}` cannot have formal parameters.")
292 end
293
294 res = p
295 if ntype.n_kwnullable != null then res = res.as_nullable
296 ntype.mtype = res
297 return res
298 end
299 end
300
301 # Check class
302 var mclass = try_get_mclass_by_qid(qid, mmodule)
303 if mclass != null then
304 var arity = ntype.n_types.length
305 if arity != mclass.arity then
306 if arity == 0 then
307 error(ntype, "Type Error: `{mclass.signature_to_s}` is a generic class.")
308 else if mclass.arity == 0 then
309 error(ntype, "Type Error: `{name}` is not a generic class.")
310 else
311 error(ntype, "Type Error: expected {mclass.arity} formal argument(s) for `{mclass.signature_to_s}`; got {arity}.")
312 end
313 return null
314 end
315 if arity == 0 then
316 res = mclass.mclass_type
317 if ntype.n_kwnullable != null then res = res.as_nullable
318 ntype.mtype = res
319 return res
320 else
321 var mtypes = new Array[MType]
322 for nt in ntype.n_types do
323 var mt = resolve_mtype_unchecked(mmodule, mclassdef, nt, with_virtual)
324 if mt == null then return null # Forward error
325 mtypes.add(mt)
326 end
327 res = mclass.get_mtype(mtypes)
328 if ntype.n_kwnullable != null then res = res.as_nullable
329 ntype.mtype = res
330 return res
331 end
332 end
333
334 # If everything fail, then give up with class by proposing things.
335 #
336 # TODO Give hints on formal types (param and virtual)
337 # TODO How to move this in a libified autonomous code?
338
339 var all_classes = model.get_mclasses_by_name(name)
340
341 # Look for imported but invisible classes.
342 if all_classes != null then for c in all_classes do
343 if not mmodule.in_importation <= c.intro_mmodule then continue
344 if mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
345 if not qid.accept(c) then continue
346 error(ntype, "Error: class `{c.full_name}` not visible in module `{mmodule}`.")
347 return null
348 end
349
350 # Look for not imported but known classes from importable modules
351 var hints = new Array[String]
352 if all_classes != null then for c in all_classes do
353 if mmodule.in_importation <= c.intro_mmodule then continue
354 if c.intro_mmodule.in_importation <= mmodule then continue
355 if c.visibility <= private_visibility then continue
356 if not qid.accept(c) then continue
357 hints.add "`{c.intro_mmodule.full_name}`"
358 end
359 if hints.not_empty then
360 error(ntype, "Error: class `{name}` not found in module `{mmodule}`. Maybe import {hints.join(",", " or ")}?")
361 return null
362 end
363
364 # Look for classes with an approximative name.
365 var bestd = name.length / 2 # limit up to 50% name change
366 for c in model.mclasses do
367 if not mmodule.in_importation <= c.intro_mmodule then continue
368 if not mmodule.is_visible(c.intro_mmodule, c.visibility) then continue
369 var d = name.levenshtein_distance(c.name)
370 if d <= bestd then
371 if d < bestd then
372 hints.clear
373 bestd = d
374 end
375 hints.add "`{c.full_name}`"
376 end
377 end
378 if hints.not_empty then
379 error(ntype, "Error: class `{name}` not found in module `{mmodule}`. Did you mean {hints.join(",", " or ")}?")
380 return null
381 end
382
383 error(ntype, "Error: class `{name}` not found in module `{mmodule}`.")
384 return null
385 end
386
387 # Return the static type associated to the node `ntype`.
388 # `mmodule` and `mclassdef` is the context where the call is made (used to understand formal types)
389 # In case of problem, an error is displayed on `ntype` and null is returned.
390 # FIXME: the name "resolve_mtype" is awful
391 fun resolve_mtype(mmodule: MModule, mclassdef: nullable MClassDef, ntype: AType): nullable MType
392 do
393 var mtype = ntype.mtype
394 if mtype == null then mtype = resolve_mtype_unchecked(mmodule, mclassdef, ntype, true)
395 if mtype == null then return null # Forward error
396
397 if ntype.checked_mtype then return mtype
398 if mtype isa MGenericType then
399 var mclass = mtype.mclass
400 for i in [0..mclass.arity[ do
401 var intro = mclass.try_intro
402 if intro == null then return null # skip error
403 var bound = intro.bound_mtype.arguments[i]
404 var nt = ntype.n_types[i]
405 var mt = resolve_mtype(mmodule, mclassdef, nt)
406 if mt == null then return null # forward error
407 var anchor
408 if mclassdef != null then anchor = mclassdef.bound_mtype else anchor = null
409 if not check_subtype(nt, mmodule, anchor, mt, bound) then
410 error(nt, "Type Error: expected `{bound}`, got `{mt}`.")
411 return null
412 end
413 end
414 end
415 ntype.checked_mtype = true
416 return mtype
417 end
418
419 # Check that `sub` is a subtype of `sup`.
420 # Do not display an error message.
421 #
422 # This method is used a an entry point for the modelize phase to test static subtypes.
423 # Some refinements could redefine it to collect statictics.
424 fun check_subtype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
425 do
426 return sub.is_subtype(mmodule, anchor, sup)
427 end
428
429 # Check that `sub` and `sup` are equvalent types.
430 # Do not display an error message.
431 #
432 # This method is used a an entry point for the modelize phase to test static equivalent types.
433 # Some refinements could redefine it to collect statictics.
434 fun check_sametype(node: ANode, mmodule: MModule, anchor: nullable MClassType, sub, sup: MType): Bool
435 do
436 return sub.is_subtype(mmodule, anchor, sup) and sup.is_subtype(mmodule, anchor, sub)
437 end
438 end
439
440 redef class ANode
441 # The indication that the node did not pass some semantic verifications.
442 #
443 # This simple flag is set by a given analysis to say that the node is broken and unusable in
444 # an execution.
445 # When a node status is set to broken, it is usually associated with a error message.
446 #
447 # If it is safe to do so, clients of the AST SHOULD just skip broken nodes in their processing.
448 # Clients that do not care about the executability (e.g. metrics) MAY still process the node or
449 # perform specific checks to determinate the validity of the node.
450 #
451 # Note that the broken status is not propagated to parent or children nodes.
452 # e.g. a broken expression used as argument does not make the whole call broken.
453 var is_broken = false is writable
454 end
455
456 redef class AType
457 # The mtype associated to the node
458 var mtype: nullable MType = null
459
460 # Is the mtype a valid one?
461 var checked_mtype: Bool = false
462 end
463
464 redef class AVisibility
465 # The visibility level associated with the AST node class
466 fun mvisibility: MVisibility is abstract
467 end
468 redef class AIntrudeVisibility
469 redef fun mvisibility do return intrude_visibility
470 end
471 redef class APublicVisibility
472 redef fun mvisibility do return public_visibility
473 end
474 redef class AProtectedVisibility
475 redef fun mvisibility do return protected_visibility
476 end
477 redef class APrivateVisibility
478 redef fun mvisibility do return private_visibility
479 end
480
481 redef class ADoc
482 private var mdoc_cache: nullable MDoc
483
484 # Convert `self` to a `MDoc`
485 fun to_mdoc: MDoc
486 do
487 var res = mdoc_cache
488 if res != null then return res
489 res = new MDoc(location)
490 for c in n_comment do
491 var text = c.text
492 if text.length < 2 then
493 res.content.add ""
494 continue
495 end
496 assert text.chars[0] == '#'
497 if text.chars[1] == ' ' then
498 text = text.substring_from(2) # eat starting `#` and space
499 else
500 text = text.substring_from(1) # eat atarting `#` only
501 end
502 if text.chars.last == '\n' then text = text.substring(0, text.length-1) # drop \n
503 res.content.add(text)
504 end
505 mdoc_cache = res
506 return res
507 end
508 end
509
510 redef class AQclassid
511 # The name of the package part, if any
512 fun mpackname: nullable String do
513 var nqualified = n_qualified
514 if nqualified == null then return null
515 var nids = nqualified.n_id
516 if nids.length <= 0 then return null
517 return nids[0].text
518 end
519
520 # The name of the module part, if any
521 fun mmodname: nullable String do
522 var nqualified = n_qualified
523 if nqualified == null then return null
524 var nids = nqualified.n_id
525 if nids.length <= 1 then return null
526 return nids[1].text
527 end
528
529 # Does `mclass` match the full qualified name?
530 fun accept(mclass: MClass): Bool
531 do
532 if mclass.name != n_id.text then return false
533 var mpackname = self.mpackname
534 if mpackname != null then
535 var mpackage = mclass.intro_mmodule.mpackage
536 if mpackage == null then return false
537 if mpackage.name != mpackname then return false
538 var mmodname = self.mmodname
539 if mmodname != null and mclass.intro_mmodule.name != mmodname then return false
540 end
541 return true
542 end
543 end