Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RuboCop: LSP and Prism

RuboCop: LSP and Prism

Koichi ITO

May 16, 2024
Tweet

More Decks by Koichi ITO

Other Decks in Programming

Transcript

  1. !LPJD w 044QSPHSBNNFS w 3VCP$PQDPSFUFBN w &OHJOFFSJOH.BOBHFSBOE %JTUJOHVJTIFE&OHJOFFSPG&4. *OD w

    3VCZ,BJHJTQFBLFSBU  -5 5BLFPVU  5BLFPVU   BOE
  2. 4 Q FBLFS 4QFBLFSTBOE"UUFOEFFT 4 Q FBLFS -5 , BSBPLF

    !LPJD !IBSVHVDIJZVNB !KVOL !4)(".&-*/,4 !GVHBLLCO !NIJSBUB !TGKXS !UIBUB !LBTVNJQPO !XBJEPJ
  3. w !LPJD1BSTFSHFNDPNNJUUFS w !KVOL-SBNBDPNNJUUFS w !4)(".&-*/,4QBSTFZDPOUSJCVUPS w !GVHBLLCO3#4DPOUSJCVUPS w .FNCFST8BOUFE

    1BSTFS$MVC IUUQTNBHB[JOFSVCZJTUOFUBSUJDMFT'PMMPX6Q,JSJTIJNB3VCZIUNM ߏจղੳثݚڀ෦
  4. w *XBTOURVJUFTVSFXIBU-41XBT  TP*XBTUIJOLJOHPGUSZJOHUPDSFBUFPOF w 5IF4UBOEBSEHFNTVQQPSUT-41 CZ!XJMM  w "U3VCZ,BJHJ

    UIFSFXBTBEJTDVTTJPO XJUI+VTUJO4FBSMTBCPVUUIFJEFBPGIBWJOH TPNFUIJOHTJNJMBSJO3VCP$PQ .PUJWBUJPO#BDLHSPVOE
  5. w 3VCZ TU  w 3BJMT UI  w 3VCP$PQ

    OE  w -41 UI )JTUPSZ -41JTB3FMBUJWFMZ/FX5FDIOPMPHZ
  6. -BOHVBHF4FSWFS1SPUPDPM &NBDT 7JN 74$PEF 4UFFQ 3VCZ-41 3VCP$PQ $MJFOU 4FSWFS 1SPUPDPM

    JSON-RPC 1SPUPDPMTNFUIPETTVDIBTinitialize textDocument/formatting BOE workspace/executeCommandBSFBNPOHUIPTFEF fi OFEJOUIF-41
  7. w 3VCZ-41 3VCZ-413BJMT Monolithic LSP  w 3VCP$PQTCVJMUJO-41 w 4UBOEBSETCVJMUJO-41

    w 4UFFQTCVJMUJO-41 w 5ZQF1SPGTCVJMUJO-41 3VCZT-41TBSF$PNQPTBCMF Micro LSP
  8. +40/31$ [jsonrpc] D[15:18:14.117] Running language server: bundle exec rubocop --lsp

    [jsonrpc] e[15:18:14.118] --> initialize[1] {"jsonrpc":"2.0","id":1,"method":"initialize","params": {"processId":3303,"clientInfo":{"name":"Eglot","version":"1.17"},"rootPath":"/ Users/koic/src/github.com/rubocop/rubocop/","rootUri":"file:///Users/koic/src/ github.com/rubocop/rubocop","initializationOptions":{},"capabilities": {"workspace":{"applyEdit":true,"executeCommand": {"dynamicRegistration":false},"workspaceEdit": {"documentChanges":true},"didChangeWatchedFiles": {"dynamicRegistration":true},"symbol": {"dynamicRegistration":false},"configuration":true,"workspaceFolders":true},"te xtDocument":{"synchronization": {"dynamicRegistration":false,"willSave":true,"willSaveWaitUntil":true,"didSave" :true},"completion":{"dynamicRegistration":false,"completionItem": {"snippetSupport":false,"deprecatedSupport":true,"resolveSupport": {"properties":["documentation","details","additionalTextEdits"]},"tagSupport": {"valueSet":[1]}},"contextSupport":true},"hover": M-x eglot-events-buffer
  9. &YBNQMFTPG-41$MJFOU (require 'eglot) (add-to-list 'eglot-server-programs '(ruby-mode . ("bundle" "exec" "rubocop"

    "--lsp"))) (add-hook 'ruby-mode-hook (lambda () (add-hook 'before-save-hook 'eglot-format-buffer nil 'local))) { "languageserver": { "rubocop": { "command": "bundle", "args" : ["exec", "rubocop", "--lsp"], "filetypes": ["ruby"], "rootPatterns": [".git", "Gemfile"], "requireRootPattern": true } }, "coc.preferences.formatOnSave": true } &NBDT FHMPU ~/.eamcs.d/init.el 7JNBOE/FPWJN DPDOWJN ~/.config/nvim/coc-settings.json
  10. %FTJHOPG3VCP$PQT-41 Request (JSON-RPC) 3VCP$PQ 3VOOFS 3VCP$PQ-41 4FSWFS 3VCP$PQ-41 3PVUFT 3VCP$PQ-41

    3VOUJNF Daemon 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp TUBSU SVO P ff FOTFT GPSNBU XSJUF DBMM SFRVFTU EFMFHBUF P ff FOTFT OFX@UFYU +40/ Response (JSON-RPC) 1MVHJO &EJUPS*%&
  11. Daemon rubocop --lsp4UBSUTB%BFNPO 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp 1MVHJO &EJUPS*%&

    $ ps aux | grep rubocop user 7414 0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop IUUQTTQFBLFSEFDLDPNLPJDNBLFSVCPDPQTVQFSGBTU TMJEF
  12. w 50%0Ϋϥεਤʁ 3VCP$PQ$-*$PNNBOE-41 module RuboCop class CLI module Command #

    Start Language Server Protocol of RuboCop. # @api private class LSP < Base self.command_name = :lsp def run RuboCop::LSP::Server.new(@config_store).start end end end $PEFTOJQQFU 3VOBTBEBFNPO $BMMFECZrubocop --lsp
  13. Daemon 4UBSUUIF-414FSWFS 3VCP$PQ-41 4FSWFS rubocop --lsp TUBSU 1MVHJO &EJUPS*%& $

    ps aux | grep rubocop user 7414 0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop 3VCP$PQ$-* $PNNBOE -41
  14. w 50%0Ϋϥεਤʁ 3VCP$PQ-414FSWFS LSP = LanguageServer::Protocol class RuboCop::LSP::Server def initialize(config_store)

    RuboCop::LSP.enable @reader = ::LSP::Transport::Io::Reader.new($stdin) @writer = ::LSP::Transport::Io::Writer.new($stdout) @runtime = RuboCop::LSP::Runtime.new(config_store) @routes = Routes.new(self) end end $PEFTOJQQFU LSPJTTIPSUGPSLanguageServer::Protocol &OBCMF-41DPOUFYU -41DMJFOUJOUFSBDUTWJBstdinBOEstdout
  15. w 50%0Ϋϥεਤʁ 3VCP$PQ-414FSWFS class RuboCop::LSP::Server def start @reader.read do |request|

    if !request.key?(:method) @routes.handle_method_missing(request) elsif (route = @routes.for(request[:method])) route.call(request) else @routes.handle_unsupported_method(request) end rescue StandardError => e # snip end $PEFTOJQQFU 1SPDFTTBSFRVFTUNFUIPE 8BJUGPSstdinJOQVUVTJOH-41*0SFBEFS 5IFrequestWBSJBCMFJTBIBTI DPOWFSUFEGSPN+40/31$
  16. Daemon 3FRVFTU +40/31$ $ ps aux | grep rubocop user

    7414 0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop Request (JSON-RPC) 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp TUBSU 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%&
  17. Daemon 3PVUJOHCZ.FUIPE $ ps aux | grep rubocop user 7414

    0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop Request (JSON-RPC) DBMM SFRVFTU 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp TUBSU 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%& 3VCP$PQ-41 3PVUFT
  18. w 50%0Ϋϥεਤʁ 3VCP$PQ-413PVUFT class RuboCop::LSP::Routes handle 'textDocument/didChange' do |request| doc

    = request[:params][:textDocument] result = diagnostic(doc[:uri], doc[:text]) @server.write(result) end handle 'textDocument/formatting' do |request| uri = request[:params][:textDocument][:uri] @server.write( id: request[:id], result: format_file(uri) ) end end 3FUSJFWFBGPSNBUUFEUFYUGSPN3VCP$PQ $PEFTOJQQFU (FUBSFTVMUPGEJBHOPTUJDGSPN3VCP$PQ
  19. Daemon 3VO3VCP$PQ $ ps aux | grep rubocop user 7414

    0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop Request (JSON-RPC) 3VCP$PQ 3VOOFS SVO P ff FOTFT GPSNBU XSJUF DBMM SFRVFTU EFMFHBUF 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp TUBSU 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%& 3VCP$PQ-41 3PVUFT 3VCP$PQ-41 3VOUJNF
  20. w 50%0Ϋϥεਤʁ 3VCP$PQ-413VOUJNF class RuboCop::LSP::Runtime def offenses(path, text) opts =

    {stdin: text, force_exclusion: true, formatters: ['json'], format: 'json'} json = redirect_stdout { run_rubocop(opts, path) } results = JSON.parse(json, symbolize_names: true) # snip end def run_rubocop(options, path) runner = RuboCop::Runner.new(options,@config_store) runner.run([path]) end $PEFTOJQQFU 3VOrubocopVTJOH3VOOFS"1* 3VOrubocop --stdin ... --format=json
  21. Daemon 3FTQPOTF +40/31$ $ ps aux | grep rubocop user

    7414 0.0 0.2 57716 4376 ?? Ss 4:48PM 0:02.13 rubocop --lsp path/to/rubocop Request (JSON-RPC) 3VCP$PQ 3VOOFS SVO P ff FOTFT GPSNBU XSJUF DBMM SFRVFTU EFMFHBUF P ff FOTFT OFX@UFYU +40/ Response (JSON-RPC) 3VCP$PQ-41 3PVUFT 3VCP$PQ$-* $PNNBOE -41 rubocop --lsp TUBSU 3VCP$PQ-41 4FSWFS 1MVHJO &EJUPS*%& 3VCP$PQ-41 3VOUJNF
  22. w 50%0Ϋϥεਤʁ 3VCP$PQ-413PVUFT class RuboCop::LSP::Routes handle 'textDocument/didChange' do |request| doc

    = request[:params][:textDocument] result = diagnostic(doc[:uri], doc[:text]) @server.write(result) end handle 'textDocument/formatting' do |request| uri = request[:params][:textDocument][:uri] @server.write( id: request[:id], result: format_file(uri) ) end end $PEFTOJQQFU (FUBSFTVMUPGEJBHOPTUJDGSPN3VCP$PQ 3FUSJFWFBGPSNBUUFEUFYUGSPN3VCP$PQ 8SJUFSFTVMUUPstdoutVTJOH-41*08SJUFS 8SJUFBVUPDPSSFDUFEDPEFUPstdoutVTJOH-41*08SJUFS
  23. -41"DDFMFSBUFT%FW9 $* &EJUPS*%& $-* &EJUPS*%& $-* &EJUPS*%& $-* &EJUPS*%&XJUI-41 $-*

    A B Smarter Faster BSFQSFTFOUTUIFEFWFMPQNFOU fl PXFOWJTJPOFEGPSUIFNBJOUPQJD &OTVSFGBTUGFFECBDLXJUIBEF fi OFEQSPUPDPM[Simple, Easy] /PSFBMUJNFGFFECBDLEVSJOHDPEJOH switch switch switch switch context switch context switch
  24. w %FWFMPQXJUINVMUJQMF-41DMJFOUTJOBXBZUIBUJT DIBSBDUFSJTUJDPG-41 w 3VCP$PQTCVJMUJO-41IBTCFFOEPHGPPEJOHXJUI &NBDT &HMPU BOETFFNTUPIBWFBTJHOJ fi DBOU

    TIBSFDPNQBUJCMFXJUI74$PEF w "TFYQFDUFEGSPNB.JDSPTPGUTQFDJ fi DBUJPO UIFSFJT BCVOEBOUJNQMFNFOUBUJPOJOGPSNBUJPOGPS74$PEF *NQMFNFOUBUJPO5JQT
  25. w "VUPDPSSFDUXJUIFormat on Save w 6OMJLFSVOOJOHUFTUT BVUPDPSSFDUPO fi MFTBWF TFFNTHFOFSBMMZTBUJTGBDUPSZ

    w &YFDVUFDPNNBOET*GVTJOH74$PEF SVOGSPN UIF$PNNBOE1BMFUUF 3FDPNNFOE"VUPDPSSFDUJO:PVS&EJUPS
  26. w 8IBUJTEF fi OFEJTNFSFMZUIFQSPUPDPM TQFDJ fi DBUJPO w 5IFEFTJHOPGMBOHVBHFTFSWFSJTMFGUUP UIFEJTDSFUJPOPGEFWFMPQFST

    w %PFT/FXNFBOUIBUUIFSFBSFTUJMM TPNFSPVHIFEHFT 🤔 -41JTB3FMBUJWFMZ/FX5FDIOPMPHZ
  27. w *GJUTWJB-41 JUTBMXBZTTFFOBTTPVSDF DPEFCFJOHFEJUFE w *GJUTWJBDPNNBOE MJOF JUTTFFOBT TPVSDFDPEFXIFSF FEJUJOHJTDPNQMFUFE

    %FDJEFCBTFEPO)PXUIF5PPMJT6TFE LSP CLI RuboCop::LSP.enabled? is true RuboCop::LSP.enabled? is false
  28. w AutoCorrect: always&OBCMFBVUPDPSSFDUJPOBMXBZT w AutoCorrect: disabled%JTBCMFBVUPDPSSFDUJPO w AutoCorrect: contextual <NEW>&OBCMF

    BVUPDPSSFDUJPOXIFOMBVODIFEGSPNUIFrubocop DPNNBOE CVUJUJT/05BWBJMBCMFUISPVHI-41 5IF5ISFF7BMVFTPG"VUP$PSSFDU &OBCMF"VUP$PSSFDU %JTBCMF"VUP$PSSFDU $POUFYUVBM 3VCP$PQ USVF GBMTF  3VCP$PQ BMXBZTPSUSVF EJTBCMFEPSGBMTF DPOUFYVBM[NEW]
  29. w *OUFSFTUFEJOXIBU&SSPS5PMFSBODFDPVME BDIJFWF CVUOPUTVQQPSUFECZUIF1BSTFS HFN w RuboCop runs 4 times

    faster with Prism JOSVCPDPQSVCPDPQ w 'BTUBOEOPCPEZDPNQMBJOT .PUJWBUJPO#BDLHSPVOE
  30. The header of lib/prism.rb # The Prism Ruby parser. #

    # "Parsing Ruby is suddenly manageable!" # - You, hopefully # module Prism 1BSTJOH3VCZJTTVEEFOMZNBOBHFBCMF w $SFBUFECZ,FWJO/FXUPO w )BOEXSJUUFO3FDVSTJWF%FTDFOU3VCZ1BSTFSJO$ w 1SPWJEF&SSPS5PMFSBODF 5SBOTMBUJPOMBZFS BOEPUIFST
  31. w :PVDBOVTF1SJTNBTUIFQBSTFSGPSUIF .3*QSPDFTTJOHXJUI--parser=prism 1BSTF7BSJPVT3VCJFTXJUI1SJTN $ ruby --parser=prism example.rb $ RUBYOPT='--parser=prism'

    ruby example.rb w 3VCZ"1*TBSFQSPWJEFEUPQBSTFTPVSDF DPEFBOEIBOEMFQBSTJOHSFTVMUT Prism.parse('articles.map { it.title }') Prism.parse_file('example.rb')
  32. w 5XPNBJODBOEJEBUFTJO6OJWFSTBM1BSTFS -SBNB -3 BOE1SJTN )BOEDSBGU  w 8IJDIXJMMCFUIF6OJWFSTBM1BSTFS 

    0OMZUJNFXJMMUFMM🕰 w &JUIFSXBZ JUTFYQFDUFEUPBEPQU UIF1SJTN"1* FH "45 IUUQTNBHB[JOFSVCZJTUOFUBSUJDMFT'PMMPX6Q,JSJTIJNB3VCZIUNM 5IF3VCZ1BSTFS$PNNVOJUZ
  33. 1BSTFSHFNBOE1SJTNT"45T Parser gem Prism 4UBUFNFOUT/PEF 1SPHSBN/PEF $BMM/PEF "SHVNFOUT/PEF 4USJOH/PEF TFOE

    TUS w 5IFTUSVDUVSF  OBNF BUUSJCVUFT  BOENFUIPETBSFBMM EJ ff FSFOU w "45VTFSTXJMMGBDF BNBTTJWFBNPVOUPG SFQMBDFNFOUXPSL puts 'hello'
  34. <<generate>> SVCZZ 5IJTDMBTTJOIFSJUJOHGSPN Racc::ParserDMBTT 1BSTFS#BTF *NQPSUQBSTFZBT3BDDQFS3VCZWFSTJPO 1BSTFS3VCZ (FOFSBUFQBSTFSDPEF CBTFEPOBTZOUBY fi

    MF BDDPSEJOHUP-"-3  3VCP$PQ"45 1SPDFTTFE4PVSDF 1BSTFS3VCZ 1BSTFS3VCZ $ racc SVCZZ <<use>> <<create>> <<read>> <<read>> <<generate>> whitequark/parser
  35. 1BSTFS#BTF *UTVQQPSUTNVMUJWFSTJPOT SVCZQSJTN 1SJTN5SBOTMBUJPO1BSTFS 5IFWBMVFCBTFEPO TargetRubyVersionJTQBTTFEUP UIF:versionPQUJPOPG1SJTN 1SJTN 3VCP$PQ"45 1SPDFTTFE4PVSDF

    1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS <<use>> <<create>> <<use>> *UCSBODIFTJOUFSOBMMZ CBTFEPOUIFWFSTJPO
  36. ⏱YJTFaster" $ cd path/to/ruby/prism $ bin/prism benchmark lib/prism.rb (snip) Comparison:

    Prism: 6951.8 i/s Ripper::SexpBuilder: 2058.9 i/s - 3.38x slower Prism::Translation::RubyParser: 1573.8 i/s - 4.42x slower Prism::Translation::Parser: 767.7 i/s - 9.06x slower Prism::Translation::Ripper::SexpBuilder: 696.0 i/s - 9.99x slower RubyParser: 221.1 i/s - 31.44x slower Parser::CurrentRuby: 210.1 i/s - 33.09x slower w 1SJTNJT33xGBTUFSUIBO1BSTFS$VSSFOU3VCZ XIJUFRVBSL  w 1SJTNJT9xGBTUFSUIBO5SBOTMBUJPO1BSTFS w 5SBOTMBUJPO1BSTFSJT3.65xGBTUFSUIBO1BSTFS$VSSFOU3VCZ
  37. &SSPS5PMFSBODFPG1SJTN $ ruby -rprism -e 'p Prism.parse("(42)").value' @ ProgramNode (location:

    (1,0)-(1,4)) ├── locals: [] └── statements: @ StatementsNode (location: (1,0)-(1,4)) └── body: (length: 1) └── @ ParenthesesNode (location: (1,0)-(1,4)) ├── body: │ @ StatementsNode (location: (1,1)-(1,3)) │ └── body: (length: 1) │ └── @ IntegerNode (location: (1,1)-(1,3)) │ ├── flags: decimal │ └── value: 42 ├── opening_loc: (1,0)-(1,1) = "(" └── closing_loc: (1,3)-(1,4) = ")" $ ruby -rprism -e 'p Prism.parse("(42").value' @ ProgramNode (location: (1,0)-(1,3)) ├── locals: [] └── statements: @ StatementsNode (location: (1,0)-(1,3)) └── body: (length: 1) └── @ ParenthesesNode (location: (1,0)-(1,3)) ├── body: │ @ StatementsNode (location: (1,1)-(1,3)) │ └── body: (length: 2) │ ├── @ IntegerNode (location: (1,1)-(1,3)) │ │ ├── flags: decimal │ │ └── value: 42 │ └── @ MissingNode (location: (1,1)-(1,3)) ├── opening_loc: (1,0)-(1,1) = "(" └── closing_loc: (1,3)-(1,3) = "" Parse the valid syntax (42) Parse the invalid syntax (42 8IFOQBSTJOHJOWBMJETZOUBY /0&3303PDDVST BOEBMissingNodeJTJOTFSUFEJOTUFBE
  38. 1BSTFS#BTF w 6TF1SJTNXJUIUIF1BSTFSHFNJOUFSGBDF 1SJTN5SBOTMBUJPO1BSTFS "IBOEXSJUUFO3VCZ QBSTFSJO$FYUFOTJPO "DMBTTUIBUTFSWFTBTBO "EBQUFSCFUXFFOUIF 1BSTFSHFNBOE1SJTN 1SJTN

    3VC$PQ"45 1SPDFTTFE4PVSDF 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS <<use>> <<create>> <<use>>
  39. 1BSTFS#BTF w 6TF1SJTNXJUIUIF1BSTFSHFNJOUFSGBDF 1SJTN5SBOTMBUJPO1BSTFS "IBOEXSJUUFO3VCZ QBSTFSJO$FYUFOTJPO "DMBTTUIBUTFSWFTBTBO "EBQUFSCFUXFFOUIF 1BSTFSHFNBOE1SJTN 1SJTN

    3VC$PQ"45 1SPDFTTFE4PVSDF 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS <<use>> <<create>> <<use>>
  40. 1BSTFS#BTF w 6TF1SJTNXJUIUIF1BSTFSHFNJOUFSGBDF 1SJTN "IBOEXSJUUFO3VCZ QBSTFSJO$FYUFOTJPO "DMBTTUIBUTFSWFTBTBO "EBQUFSCFUXFFOUIF 1BSTFSHFNBOE1SJTN 1SJTN

    3VC$PQ"45 1SPDFTTFE4PVSDF 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS 1SJTN5SBOTMBUJPO 1BSTFS <<use>> <<create>> <<use>>
  41. <<generate>> QBSTFZ  %FQFOEPO"CTUSBDUJPO BOE4UBCJMJUZ 1SJTN"1* 5SBOTMBUJPO-BZFS 3VCZQBSTFSHFOFSBUFECBTFEPOUIFQBSTFZGPSFBDI3VCZWFSTJPO 0S1SJTN"1*XJUI-SBNB (FOFSBUFQBSTFSDPEF

    CBTFEPOBTZOUBY fi MF BDDPSEJOHUP14-3  3VCP$PQ"45 1SPDFTTFE4PVSDF "3VCZ1BSTFS QBSTFD QBSTFTP GPS3VCZ "3VCZ1BSTFS QBSTFD QBSTFTP GPS3VCZ $ lrama QBSTFZ  <<use>> <<require>> <<read>> <<read>> <<generate>> ruby/ruby
  42. w .PSF'BTUFS w (BJO&SSPS5PMFSBODF w "MNPTUOPOFFEGPS1BSTFSHFNNBJOUFOBODF XPParser::Source::TreeRewriter  w 5IFUniversal

    ParserXJMMFOBCMFUIFBCJMJUZUP SVOUIFTBNFQBSTFSVOJWFSTBMMZJOUIFGVUVSF #FOF fi UTPG.JHSBUJOHUP7BOJMMB1SJTN
  43. 3VCP$PQT1BSTFS&OHJOFT 1BSTFSHFN✅ 1SJTN5SBOTMBUJPO1BSTFS✅ 1SJTN "1* ☑ 5IF/BNFPG 1BSTFS&OHJOF QBSTFS@XIJUFRVBSL QBSTFS@QSJTN

    QSJTN 1BSTJOH4QFFE  YdY Y 4VQQPSUGPS 1BSTJOH3VCZ 3VCZ 3VCZ 3VCZ  &SSPS5PMFSBODF /FWFS4VQQPSUFE /PU4VQQPSUFE 4VQQPSUFE 6OJWFSTBM1BSTFS /P "NCJHVPVT :FT %FWFMPQNFOU 4UBUVT 4UBCMF &YQFSJNFOUBM $PODFQUVBM ✅4VQQPSUFE ☑6OTVQQPSUFE
  44. w (FO3FHFYQ w (FO3JQQFS w (FOXIJUFRVBSLQBSTFS w (FO1SJTN5SBOTMBUJPO1SJTN w (FO1SJTN"1*

     TBD 1BSTFS(FOFSBUJPOT"HBJOTU3VCP$PQ IUUQTSVCZLBJHJPSHQSFTFOUBUJPOTCCBUTPWIUNMNBZ